Compare commits

...

29 Commits

Author SHA1 Message Date
Lennart
283be0a26c version 0.4.5 2025-06-27 17:40:48 +02:00
Lennart
1060625b9d fix(oidc): Fix login not working for missing groups claim
see #87
2025-06-27 17:38:42 +02:00
Lennart
86ae31e94c tiny steps towards unit testing for each resource 2025-06-27 14:33:25 +02:00
Lennart
e2f5773e3c Dockerfile: Target Rust 1.88 2025-06-27 14:32:08 +02:00
Lennart
b54fbebe7c store: test preparations 2025-06-27 13:58:14 +02:00
Lennart
fe78a82806 clippy appeasement 2025-06-27 13:57:57 +02:00
Lennart
22544b8c2f Justfile: Add commands to build frontend components 2025-06-27 13:57:44 +02:00
Lennart
340b99e491 Dockerfile: Fix llvm dependency for arm64 builds 2025-06-26 22:25:04 +02:00
Lennart
787ea90376 Dockerfile, update Rust to 1.87+ 2025-06-26 22:03:16 +02:00
Lennart
973a86f21a remove some disabled and broken tests 2025-06-26 19:42:06 +02:00
Lennart
39fc2fb55d principal_type refactoring 2025-06-26 12:50:37 +02:00
Lennart
ab4d763304 tiny improvements to documentation 2025-06-26 12:39:23 +02:00
Lennart
9cf74f7198 frontend: Explicitly mark collections from other groups 2025-06-25 16:14:55 +02:00
Lennart
2c2a6006c7 version 0.4.4 2025-06-25 16:03:31 +02:00
Lennart
4600f03b45 frontend: slight improvements to collection lists 2025-06-25 16:01:17 +02:00
Lennart
41fc1e6ea5 frontend: Remove default red for calendars without color 2025-06-25 15:56:46 +02:00
Lennart
b56591c482 frontend: LSP appeasement 2025-06-25 15:54:47 +02:00
Lennart
d639b18005 version 0.4.3 2025-06-23 16:44:21 +02:00
Lennart
6046439fc7 feat(dav): Add show_deleted parameter to get_resource
Fixes #86
2025-06-23 16:43:46 +02:00
Lennart
f9de8a4687 feat: Add show_deleted to get_calendar 2025-06-23 16:35:36 +02:00
Lennart
8dfb47b28f version 0.4.2 2025-06-23 16:13:18 +02:00
Lennart
eb720ded99 ci: Only tag releases as latest container images 2025-06-23 16:12:36 +02:00
Lennart
89ef7b2ced Update vcard date tests 2025-06-23 16:09:22 +02:00
Lennart
6e0129130e Fix birthdays without year in birthday calendar
Fixes #79
2025-06-23 16:03:59 +02:00
Lennart
c646986c56 Version 0.4.1 2025-06-23 14:08:06 +02:00
Lennart
503cbe3699 fix: Add default frontend config 2025-06-23 14:07:38 +02:00
Lennart
79c66a0b46 fix(caldav): Fix permissions to allow for deletion of calendar subscriptions
fixes #84
2025-06-23 14:04:09 +02:00
Lennart
e5687c6e43 fix(frontend): calendar subscription creation 2025-06-23 14:03:10 +02:00
Lennart
79b67a17c3 Implement deletion button to permanently delete collections 2025-06-23 13:48:00 +02:00
77 changed files with 4667 additions and 4437 deletions

View File

@@ -2,3 +2,5 @@
indent_style = space
indent_size = 4
[docs/**/*.md]
indent_size = 4

View File

@@ -41,12 +41,10 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# As long as we don't have releases everything on the main branch shall be tagged as latest
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT *\n FROM calendars\n WHERE (principal, id) = (?, ?)",
"query": "SELECT *\n FROM calendars\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?) ",
"describe": {
"columns": [
{
@@ -80,7 +80,7 @@
}
],
"parameters": {
"Right": 2
"Right": 3
},
"nullable": [
false,
@@ -100,5 +100,5 @@
false
]
},
"hash": "9f930775043a6d4571a8ffd5a981cadf7c51f3f11a189f8461505abec31076e6"
"hash": "bb2fa030f2e7c7afdb38c5c54cb31de5293be332d86cf643977d479999542553"
}

268
Cargo.lock generated
View File

@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "adler2"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
@@ -49,9 +49,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.18"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -64,33 +64,33 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.10"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.8"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
@@ -162,9 +162,9 @@ dependencies = [
[[package]]
name = "askama_web"
version = "0.14.3"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a91fdeb04bf77d96234780cdd58fc221eb10de7031e1782a22f40fc8ac1a313"
checksum = "83731f1a2286209c2b679445e8faaa53270646a90c509bf92729e966d198cb6b"
dependencies = [
"askama",
"askama_web_derive",
@@ -206,9 +206,9 @@ dependencies = [
[[package]]
name = "atomic"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
dependencies = [
"bytemuck",
]
@@ -221,9 +221,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
@@ -379,15 +379,15 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.17.0"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "bytemuck"
version = "1.23.0"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]]
name = "byteorder"
@@ -403,18 +403,18 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.25"
version = "1.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "cfg_aliases"
@@ -469,9 +469,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.39"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
dependencies = [
"clap_builder",
"clap_derive",
@@ -479,9 +479,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.39"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
dependencies = [
"anstream",
"anstyle",
@@ -491,9 +491,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.32"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
dependencies = [
"heck",
"proc-macro2",
@@ -503,15 +503,15 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.4"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "colorchoice"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "concurrent-queue"
@@ -1068,7 +1068,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi 0.11.1+wasi-snapshot-preview1",
"wasm-bindgen",
]
@@ -1136,9 +1136,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.3"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [
"allocator-api2",
"equivalent",
@@ -1151,7 +1151,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.3",
"hashbrown 0.15.4",
]
[[package]]
@@ -1295,9 +1295,9 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.27.6"
version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http",
"hyper",
@@ -1325,9 +1325,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.13"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -1512,7 +1512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.3",
"hashbrown 0.15.4",
"serde",
]
@@ -1589,9 +1589,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.172"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libm"
@@ -1678,9 +1678,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.7.4"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "mime"
@@ -1700,9 +1700,9 @@ dependencies = [
[[package]]
name = "miniz_oxide"
version = "0.8.8"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
@@ -1714,7 +1714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@@ -2370,9 +2370,9 @@ dependencies = [
[[package]]
name = "quinn-udp"
version = "0.5.12"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842"
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
dependencies = [
"cfg_aliases",
"libc",
@@ -2393,9 +2393,9 @@ dependencies = [
[[package]]
name = "r-efi"
version = "5.2.0"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
@@ -2458,13 +2458,33 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.12"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
dependencies = [
"bitflags",
]
[[package]]
name = "ref-cast"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -2517,9 +2537,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "reqwest"
version = "0.12.19"
version = "0.12.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -2534,11 +2554,9 @@ dependencies = [
"hyper",
"hyper-rustls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"once_cell",
"percent-encoding",
"pin-project-lite",
"quinn",
@@ -2715,9 +2733,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.24"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustc-hash"
@@ -2736,7 +2754,7 @@ dependencies = [
[[package]]
name = "rustical"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"anyhow",
"argon2",
@@ -2779,7 +2797,7 @@ dependencies = [
[[package]]
name = "rustical_caldav"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"async-trait",
"axum",
@@ -2798,6 +2816,7 @@ dependencies = [
"rustical_dav_push",
"rustical_ical",
"rustical_store",
"rustical_store_sqlite",
"rustical_xml",
"serde",
"sha2",
@@ -2814,7 +2833,7 @@ dependencies = [
[[package]]
name = "rustical_carddav"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"async-trait",
"axum",
@@ -2846,7 +2865,7 @@ dependencies = [
[[package]]
name = "rustical_dav"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"async-trait",
"axum",
@@ -2871,7 +2890,7 @@ dependencies = [
[[package]]
name = "rustical_dav_push"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"async-trait",
"axum",
@@ -2897,7 +2916,7 @@ dependencies = [
[[package]]
name = "rustical_frontend"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"askama",
"askama_web",
@@ -2930,7 +2949,7 @@ dependencies = [
[[package]]
name = "rustical_ical"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"axum",
"chrono",
@@ -2948,7 +2967,7 @@ dependencies = [
[[package]]
name = "rustical_oidc"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"async-trait",
"axum",
@@ -2963,7 +2982,7 @@ dependencies = [
[[package]]
name = "rustical_store"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"anyhow",
"async-trait",
@@ -2997,7 +3016,7 @@ dependencies = [
[[package]]
name = "rustical_store_sqlite"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"async-trait",
"chrono",
@@ -3017,7 +3036,7 @@ dependencies = [
[[package]]
name = "rustical_xml"
version = "0.3.6"
version = "0.4.5"
dependencies = [
"quick-xml",
"thiserror 2.0.12",
@@ -3026,9 +3045,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.27"
version = "0.23.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
dependencies = [
"once_cell",
"ring",
@@ -3080,6 +3099,18 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -3169,9 +3200,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.8"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
@@ -3190,15 +3221,16 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.12.0"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.9.0",
"schemars",
"serde",
"serde_derive",
"serde_json",
@@ -3208,9 +3240,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.12.0"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
dependencies = [
"darling",
"proc-macro2",
@@ -3282,18 +3314,15 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.9"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
[[package]]
name = "smallvec"
version = "1.15.0"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
@@ -3357,7 +3386,7 @@ dependencies = [
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.3",
"hashbrown 0.15.4",
"hashlink",
"indexmap 2.9.0",
"log",
@@ -3573,9 +3602,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.101"
version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",
@@ -3644,12 +3673,11 @@ dependencies = [
[[package]]
name = "thread_local"
version = "1.1.8"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
@@ -3774,9 +3802,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.22"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
@@ -3786,18 +3814,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.9"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.26"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap 2.9.0",
"serde",
@@ -3809,9 +3837,9 @@ dependencies = [
[[package]]
name = "toml_write"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tonic"
@@ -3979,9 +4007,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.28"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
@@ -3990,9 +4018,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
"valuable",
@@ -4186,9 +4214,9 @@ dependencies = [
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
@@ -4298,9 +4326,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502"
dependencies = [
"rustls-pki-types",
]
@@ -4383,9 +4411,9 @@ dependencies = [
[[package]]
name = "windows-link"
version = "0.1.1"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-result"
@@ -4555,9 +4583,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.10"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
dependencies = [
"memchr",
]
@@ -4620,18 +4648,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.25"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"]
[workspace.package]
version = "0.3.6"
version = "0.4.5"
edition = "2024"
description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical"

View File

@@ -1,11 +1,11 @@
FROM --platform=$BUILDPLATFORM rust:1.86-alpine AS chef
FROM --platform=$BUILDPLATFORM rust:1.88-alpine AS chef
ARG TARGETPLATFORM
ARG BUILDPLATFORM
# the compiler will otherwise ask for aarch64-linux-musl-gcc
ENV CC_aarch64_unknown_linux_musl="clang"
ENV AR_aarch64_unknown_linux_musl="llvm-ar"
ENV AR_aarch64_unknown_linux_musl="llvm20-ar"
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld"
# Stupid workaound with tempfiles since environment variables
@@ -16,7 +16,7 @@ RUN case $TARGETPLATFORM in \
*) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \
esac
RUN apk add --no-cache musl-dev llvm19 clang perl pkgconf make \
RUN apk add --no-cache musl-dev llvm20 clang perl pkgconf make \
&& rustup target add "$(cat /tmp/rust_target)" \
&& cargo install cargo-chef --locked \
&& rm -rf "$CARGO_HOME/registry"

View File

@@ -1,2 +1,9 @@
licenses:
cargo about generate about.hbs > crates/frontend/public/assets/licenses.html
frontend-dev:
cd crates/frontend/js-components && deno task dev
frontend-build:
cd crates/frontend/js-components && deno task build

View File

@@ -7,6 +7,9 @@ repository.workspace = true
license.workspace = true
publish = false
[dev-dependencies]
rustical_store_sqlite = { workspace = true, features = ["test"] }
[dependencies]
axum.workspace = true
axum-extra.workspace = true

View File

@@ -24,12 +24,16 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
return Err(crate::Error::Unauthorized);
}
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
let calendar = cal_store
.get_calendar(&principal, &calendar_id, true)
.await?;
if !user.is_principal(&calendar.principal) {
return Err(crate::Error::Unauthorized);
}
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
let calendar = cal_store
.get_calendar(&principal, &calendar_id, true)
.await?;
let mut timezones = HashMap::new();
let objects = cal_store.get_objects(&principal, &calendar_id).await?;

View File

@@ -25,7 +25,7 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
let calendar = resource_service
.cal_store
.get_calendar(&principal, &cal_id)
.get_calendar(&principal, &cal_id, false)
.await?;
let calendar_resource = CalendarResource {
cal: calendar,

View File

@@ -292,7 +292,12 @@ impl Resource for CalendarResource {
}
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
if self.cal.subscription_url.is_some() || self.read_only {
if self.cal.subscription_url.is_some() {
return Ok(UserPrivilegeSet::owner_write_properties(
user.is_principal(&self.cal.principal),
));
}
if self.read_only {
return Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.cal.principal),
));

View File

@@ -56,8 +56,12 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
async fn get_resource(
&self,
(principal, cal_id): &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Error> {
let calendar = self.cal_store.get_calendar(principal, cal_id).await?;
let calendar = self
.cal_store
.get_calendar(principal, cal_id, show_deleted)
.await?;
Ok(CalendarResource {
cal: calendar,
read_only: self.cal_store.is_read_only(cal_id),

View File

@@ -27,7 +27,9 @@ pub async fn get_event<C: CalendarStore>(
return Err(crate::Error::Unauthorized);
}
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
let calendar = cal_store
.get_calendar(&principal, &calendar_id, false)
.await?;
if !user.is_principal(&calendar.principal) {
return Err(crate::Error::Unauthorized);
}

View File

@@ -58,10 +58,11 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
calendar_id,
object_id,
}: &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Self::Error> {
let object = self
.cal_store
.get_object(principal, calendar_id, object_id, false)
.get_object(principal, calendar_id, object_id, show_deleted)
.await?;
Ok(CalendarObjectResource {
object,

View File

@@ -11,8 +11,10 @@ mod service;
pub use service::*;
mod prop;
pub use prop::*;
#[cfg(test)]
pub mod tests;
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct PrincipalResource {
principal: Principal,
members: Vec<String>,

View File

@@ -48,6 +48,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
async fn get_resource(
&self,
(principal,): &Self::PathComponents,
_show_deleted: bool,
) -> Result<Self::Resource, Self::Error> {
let user = self
.auth_provider

View File

@@ -0,0 +1,39 @@
use crate::principal::PrincipalResourceService;
use rustical_dav::resource::ResourceService;
use rustical_store::auth::{AuthenticationProvider, Principal, PrincipalType};
use rustical_store_sqlite::tests::get_test_stores;
#[tokio::test]
async fn test_principal_resource() {
let (_, cal_store, sub_store, auth_provider, _) = get_test_stores().await;
let service = PrincipalResourceService {
cal_store,
sub_store,
auth_provider: auth_provider.clone(),
};
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,
Err(crate::Error::NotFound)
));
let _principal_resource = service
.get_resource(&("user".to_owned(),), true)
.await
.unwrap();
}

View File

@@ -49,10 +49,11 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
addressbook_id,
object_id,
}: &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Self::Error> {
let object = self
.addr_store
.get_object(principal, addressbook_id, object_id, false)
.get_object(principal, addressbook_id, object_id, show_deleted)
.await?;
Ok(AddressObjectResource {
object,

View File

@@ -58,6 +58,7 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
Ok((result, not_found))
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
addr_multiget: &AddressbookMultigetRequest,
prop: &PropfindType<AddressObjectPropWrapperName>,

View File

@@ -59,10 +59,11 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
async fn get_resource(
&self,
(principal, addressbook_id): &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Error> {
let addressbook = self
.addr_store
.get_addressbook(principal, addressbook_id, false)
.get_addressbook(principal, addressbook_id, show_deleted)
.await
.map_err(|_e| Error::NotFound)?;
Ok(addressbook.into())

View File

@@ -59,6 +59,7 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
async fn get_resource(
&self,
(principal,): &Self::PathComponents,
_show_deleted: bool,
) -> Result<Self::Resource, Self::Error> {
let user = self
.auth_provider

View File

@@ -2,6 +2,7 @@ use quick_xml::name::Namespace;
use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::collections::{HashMap, HashSet};
// https://datatracker.ietf.org/doc/html/rfc3744
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq)]
pub enum UserPrivilege {
Read,
@@ -47,6 +48,12 @@ pub struct UserPrivilegeSet {
impl UserPrivilegeSet {
pub fn has(&self, privilege: &UserPrivilege) -> bool {
if (privilege == &UserPrivilege::WriteProperties
|| privilege == &UserPrivilege::WriteContent)
&& self.privileges.contains(&UserPrivilege::Write)
{
return true;
}
self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All)
}
@@ -72,6 +79,15 @@ impl UserPrivilegeSet {
}
}
pub fn owner_write_properties(is_owner: bool) -> Self {
// Content is read-only but we can write properties
if is_owner {
Self::write_properties()
} else {
Self::default()
}
}
pub fn read_only() -> Self {
Self {
privileges: HashSet::from([
@@ -81,6 +97,17 @@ impl UserPrivilegeSet {
]),
}
}
pub fn write_properties() -> Self {
Self {
privileges: HashSet::from([
UserPrivilege::Read,
UserPrivilege::WriteProperties,
UserPrivilege::ReadAcl,
UserPrivilege::ReadCurrentUserPrivilegeSet,
]),
}
}
}
impl<const N: usize> From<[UserPrivilege; N]> for UserPrivilegeSet {

View File

@@ -45,10 +45,11 @@ pub async fn route_delete<R: ResourceService>(
if_match: Option<IfMatch>,
if_none_match: Option<IfNoneMatch>,
) -> Result<(), R::Error> {
let resource = resource_service.get_resource(path_components).await?;
let resource = resource_service.get_resource(path_components, true).await?;
// Kind of a bodge since we don't get unbind from the parent
let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Write) {
if !privileges.has(&UserPrivilege::WriteProperties) {
return Err(Error::Unauthorized.into());
}

View File

@@ -49,7 +49,9 @@ pub(crate) async fn route_propfind<R: ResourceService>(
resource_service: &R,
puri: &impl PrincipalUri,
) -> Result<RSMultistatus<R>, R::Error> {
let resource = resource_service.get_resource(path_components).await?;
let resource = resource_service
.get_resource(path_components, false)
.await?;
let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Read) {
return Err(Error::Unauthorized.into());

View File

@@ -85,7 +85,9 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
operations,
) = XmlDocument::parse_str(body).map_err(Error::XmlError)?;
let mut resource = resource_service.get_resource(path_components).await?;
let mut resource = resource_service
.get_resource(path_components, false)
.await?;
let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Write) {
return Err(Error::Unauthorized.into());

View File

@@ -34,7 +34,8 @@ pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
async fn get_resource(
&self,
_path: &Self::PathComponents,
path: &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Self::Error>;
async fn save_resource(

View File

@@ -86,7 +86,11 @@ where
const DAV_HEADER: &str = "1, 3, access-control";
async fn get_resource(&self, _: &()) -> Result<Self::Resource, Self::Error> {
async fn get_resource(
&self,
_: &(),
_show_deleted: bool,
) -> Result<Self::Resource, Self::Error> {
Ok(RootResource::<PRS::Resource, P>::default())
}

View File

@@ -19,7 +19,7 @@ export class CreateAddressbookForm extends LitElement {
@property()
user: String = ''
@property()
id: String = ''
addr_id: String = ''
@property()
displayname: String = ''
@property()
@@ -36,7 +36,7 @@ export class CreateAddressbookForm extends LitElement {
<form @submit=${this.submit} ${ref(this.form)}>
<label>
id
<input type="text" name="id" @change=${e => this.id = e.target.value} />
<input type="text" name="id" @change=${e => this.addr_id = e.target.value} />
</label>
<br>
<label>
@@ -59,7 +59,7 @@ export class CreateAddressbookForm extends LitElement {
async submit(e: SubmitEvent) {
console.log(this.displayname)
e.preventDefault()
if (!this.id) {
if (!this.addr_id) {
alert("Empty id")
return
}
@@ -68,7 +68,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.id}`, {
await this.client.createDirectory(`/principal/${this.user}/${this.addr_id}`, {
data: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>

View File

@@ -7,7 +7,6 @@ import { createClient } from "webdav";
export class CreateCalendarForm extends LitElement {
constructor() {
super()
}
protected override createRenderRoot() {
@@ -19,7 +18,7 @@ export class CreateCalendarForm extends LitElement {
@property()
user: String = ''
@property()
id: String = ''
cal_id: String = ''
@property()
displayname: String = ''
@property()
@@ -43,7 +42,7 @@ export class CreateCalendarForm extends LitElement {
<form @submit=${this.submit} ${ref(this.form)}>
<label>
id
<input type="text" name="id" @change=${e => this.id = e.target.value} />
<input type="text" name="id" @change=${e => this.cal_id = e.target.value} />
</label>
<br>
<label>
@@ -83,7 +82,7 @@ export class CreateCalendarForm extends LitElement {
async submit(e: SubmitEvent) {
console.log(this.displayname)
e.preventDefault()
if (!this.id) {
if (!this.cal_id) {
alert("Empty id")
return
}
@@ -95,7 +94,7 @@ export class CreateCalendarForm extends LitElement {
alert("No calendar components selected")
return
}
await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
await this.client.createDirectory(`/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>
@@ -103,7 +102,7 @@ export class CreateCalendarForm extends LitElement {
<displayname>${this.displayname}</displayname>
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''}
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ''}
${this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ''}
<CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')}
</CAL:supported-calendar-component-set>

View File

@@ -0,0 +1,44 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createClient } from "webdav";
@customElement("delete-button")
export class DeleteButton extends LitElement {
constructor() {
super()
}
@property({ type: Boolean })
trash: boolean = false
@property()
href: string
protected createRenderRoot() {
return this
}
protected render() {
let text = this.trash ? 'Move to trash' : 'Delete'
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
}
async _onClick(event: Event) {
event.preventDefault()
if (!this.trash && !confirm('Do you want to delete this collection permanently?')) {
return
}
let response = await fetch(this.href, {
method: 'DELETE',
headers: {
'X-No-Trashbin': this.trash ? '0' : '1'
}
})
if (response.status < 200 || response.status >= 300) {
alert('An error occured, look into the console')
console.error(response)
return
}
window.location.reload()
}
}

View File

@@ -1,7 +1,7 @@
{
"module": "nodenext",
"compilerOptions": {
"target": "es2024",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"lib": [

View File

@@ -1,10 +1,11 @@
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: {
// include: ["lit"]
},
build: {
minify: false,
modulePreload: {
polyfill: false
},
copyPublicDir: false,
lib: {
entry: 'lib/index.ts',
@@ -15,6 +16,7 @@ export default defineConfig({
input: [
"lib/create-calendar-form.ts",
"lib/create-addressbook-form.ts",
"lib/delete-button.ts",
],
output: {
dir: "../public/assets/js/",

View File

@@ -1,49 +1,66 @@
import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
import { a as b } from "./webdav-Bz4I5vNH.mjs";
var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, a = (t, s, l, r) => {
for (var e = r > 1 ? void 0 : r ? f(s, l) : s, n = t.length - 1, p; n >= 0; n--)
(p = t[n]) && (e = (r ? p(s, l, e) : p(e)) || e);
return r && e && y(s, l, e), e;
import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n } from "./ref-CPp9J0V5.mjs";
import { a as an } from "./webdav-D0R7xCzX.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let i = class extends c {
let CreateAddressbookForm = class extends i {
constructor() {
super(), this.client = b("/carddav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.dialog = d(), this.form = d();
super();
this.client = an("/carddav");
this.user = "";
this.addr_id = "";
this.displayname = "";
this.description = "";
this.dialog = e();
this.form = e();
}
createRenderRoot() {
return this;
}
render() {
return u`
return x`
<button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
<dialog ${m(this.dialog)}>
<dialog ${n(this.dialog)}>
<h3>Create addressbook</h3>
<form @submit=${this.submit} ${m(this.form)}>
<form @submit=${this.submit} ${n(this.form)}>
<label>
id
<input type="text" name="id" @change=${(t) => this.id = t.target.value} />
<input type="text" name="id" @change=${(e2) => this.addr_id = e2.target.value} />
</label>
<br>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${(t) => this.description = t.target.value} />
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
</label>
<br>
<button type="submit">Create</button>
<button type="submit" @click=${(t) => {
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}}> Cancel </button>
</form>
</dialog>
`;
}
async submit(t) {
if (console.log(this.displayname), t.preventDefault(), !this.id) {
async submit(e2) {
console.log(this.displayname);
e2.preventDefault();
if (!this.addr_id) {
alert("Empty id");
return;
}
@@ -51,7 +68,7 @@ let i = class extends c {
alert("Empty displayname");
return;
}
return await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
await this.client.createDirectory(`/principal/${this.user}/${this.addr_id}`, {
data: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
@@ -62,24 +79,26 @@ let i = class extends c {
</set>
</mkcol>
`
}), window.location.reload(), null;
});
window.location.reload();
return null;
}
};
a([
o()
], i.prototype, "user", 2);
a([
o()
], i.prototype, "id", 2);
a([
o()
], i.prototype, "displayname", 2);
a([
o()
], i.prototype, "description", 2);
i = a([
h("create-addressbook-form")
], i);
__decorateClass([
n$1()
], CreateAddressbookForm.prototype, "user", 2);
__decorateClass([
n$1()
], CreateAddressbookForm.prototype, "addr_id", 2);
__decorateClass([
n$1()
], CreateAddressbookForm.prototype, "displayname", 2);
__decorateClass([
n$1()
], CreateAddressbookForm.prototype, "description", 2);
CreateAddressbookForm = __decorateClass([
t("create-addressbook-form")
], CreateAddressbookForm);
export {
i as CreateAddressbookForm
CreateAddressbookForm
};

View File

@@ -1,66 +1,86 @@
import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
import { a as b } from "./webdav-Bz4I5vNH.mjs";
var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, l, s) => {
for (var i = s > 1 ? void 0 : s ? $(e, l) : e, n = t.length - 1, p; n >= 0; n--)
(p = t[n]) && (i = (s ? p(e, l, i) : p(i)) || i);
return s && i && y(e, l, i), i;
import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n } from "./ref-CPp9J0V5.mjs";
import { a as an } from "./webdav-D0R7xCzX.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let r = class extends u {
let CreateCalendarForm = class extends i {
constructor() {
super(), this.client = b("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(), this.dialog = d(), this.form = d();
super();
this.client = an("/caldav");
this.user = "";
this.cal_id = "";
this.displayname = "";
this.description = "";
this.color = "";
this.subscriptionUrl = "";
this.components = /* @__PURE__ */ new Set();
this.dialog = e();
this.form = e();
}
createRenderRoot() {
return this;
}
render() {
return c`
return x`
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${m(this.dialog)}>
<dialog ${n(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${m(this.form)}>
<form @submit=${this.submit} ${n(this.form)}>
<label>
id
<input type="text" name="id" @change=${(t) => this.id = t.target.value} />
<input type="text" name="id" @change=${(e2) => this.cal_id = e2.target.value} />
</label>
<br>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${(t) => this.description = t.target.value} />
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${(t) => this.color = t.target.value} />
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
</label>
<br>
<label>
Subscription URL
<input type="text" name="subscription_url" @change=${(t) => this.subscriptionUrl = t.target.value} />
<input type="text" name="subscription_url" @change=${(e2) => this.subscriptionUrl = e2.target.value} />
</label>
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map((t) => c`
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
<label>
Support ${t}
<input type="checkbox" value=${t} @change=${(e) => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
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>
<button type="submit">Create</button>
<button type="submit" @click=${(t) => {
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}}> Cancel </button>
</form>
</dialog>
`;
}
async submit(t) {
if (console.log(this.displayname), t.preventDefault(), !this.id) {
async submit(e2) {
console.log(this.displayname);
e2.preventDefault();
if (!this.cal_id) {
alert("Empty id");
return;
}
@@ -72,7 +92,7 @@ let r = class extends u {
alert("No calendar components selected");
return;
}
return await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
await this.client.createDirectory(`/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>
@@ -80,42 +100,43 @@ let r = class extends u {
<displayname>${this.displayname}</displayname>
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""}
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ""}
${this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ""}
<CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map((e) => `<CAL:comp name="${e}" />`).join(`
`)}
${Array.from(this.components.keys()).map((comp) => `<CAL:comp name="${comp}" />`).join("\n")}
</CAL:supported-calendar-component-set>
</prop>
</set>
</mkcol>
`
}), window.location.reload(), null;
});
window.location.reload();
return null;
}
};
a([
o()
], r.prototype, "user", 2);
a([
o()
], r.prototype, "id", 2);
a([
o()
], r.prototype, "displayname", 2);
a([
o()
], r.prototype, "description", 2);
a([
o()
], r.prototype, "color", 2);
a([
o()
], r.prototype, "subscriptionUrl", 2);
a([
o()
], r.prototype, "components", 2);
r = a([
h("create-calendar-form")
], r);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "user", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "cal_id", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "displayname", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "color", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "subscriptionUrl", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "components", 2);
CreateCalendarForm = __decorateClass([
t("create-calendar-form")
], CreateCalendarForm);
export {
r as CreateCalendarForm
CreateCalendarForm
};

View File

@@ -0,0 +1,55 @@
import { i, x } from "./lit-z6_uA4GX.mjs";
import { n, t } from "./property-D0NJdseG.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let DeleteButton = class extends i {
constructor() {
super();
this.trash = false;
}
createRenderRoot() {
return this;
}
render() {
let text = this.trash ? "Move to trash" : "Delete";
return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`;
}
async _onClick(event) {
event.preventDefault();
if (!this.trash && !confirm("Do you want to delete this collection permanently?")) {
return;
}
let response = await fetch(this.href, {
method: "DELETE",
headers: {
"X-No-Trashbin": this.trash ? "0" : "1"
}
});
if (response.status < 200 || response.status >= 300) {
alert("An error occured, look into the console");
console.error(response);
return;
}
window.location.reload();
}
};
__decorateClass([
n({ type: Boolean })
], DeleteButton.prototype, "trash", 2);
__decorateClass([
n()
], DeleteButton.prototype, "href", 2);
DeleteButton = __decorateClass([
t("delete-button")
], DeleteButton);
export {
DeleteButton
};

View File

@@ -1,551 +0,0 @@
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const M = globalThis, B = M.ShadowRoot && (M.ShadyCSS === void 0 || M.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, tt = Symbol(), W = /* @__PURE__ */ new WeakMap();
let ot = class {
constructor(t, e, s) {
if (this._$cssResult$ = !0, s !== tt) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
this.cssText = t, this.t = e;
}
get styleSheet() {
let t = this.o;
const e = this.t;
if (B && t === void 0) {
const s = e !== void 0 && e.length === 1;
s && (t = W.get(e)), t === void 0 && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), s && W.set(e, t));
}
return t;
}
toString() {
return this.cssText;
}
};
const ht = (r) => new ot(typeof r == "string" ? r : r + "", void 0, tt), at = (r, t) => {
if (B) r.adoptedStyleSheets = t.map((e) => e instanceof CSSStyleSheet ? e : e.styleSheet);
else for (const e of t) {
const s = document.createElement("style"), i = M.litNonce;
i !== void 0 && s.setAttribute("nonce", i), s.textContent = e.cssText, r.appendChild(s);
}
}, V = B ? (r) => r : (r) => r instanceof CSSStyleSheet ? ((t) => {
let e = "";
for (const s of t.cssRules) e += s.cssText;
return ht(e);
})(r) : r;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const { is: lt, defineProperty: ct, getOwnPropertyDescriptor: dt, getOwnPropertyNames: pt, getOwnPropertySymbols: ut, getPrototypeOf: $t } = Object, f = globalThis, q = f.trustedTypes, _t = q ? q.emptyScript : "", k = f.reactiveElementPolyfillSupport, w = (r, t) => r, j = { toAttribute(r, t) {
switch (t) {
case Boolean:
r = r ? _t : null;
break;
case Object:
case Array:
r = r == null ? r : JSON.stringify(r);
}
return r;
}, fromAttribute(r, t) {
let e = r;
switch (t) {
case Boolean:
e = r !== null;
break;
case Number:
e = r === null ? null : Number(r);
break;
case Object:
case Array:
try {
e = JSON.parse(r);
} catch {
e = null;
}
}
return e;
} }, et = (r, t) => !lt(r, t), J = { attribute: !0, type: String, converter: j, reflect: !1, useDefault: !1, hasChanged: et };
Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), f.litPropertyMetadata ?? (f.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
let v = class extends HTMLElement {
static addInitializer(t) {
this._$Ei(), (this.l ?? (this.l = [])).push(t);
}
static get observedAttributes() {
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
}
static createProperty(t, e = J) {
if (e.state && (e.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((e = Object.create(e)).wrapped = !0), this.elementProperties.set(t, e), !e.noAccessor) {
const s = Symbol(), i = this.getPropertyDescriptor(t, s, e);
i !== void 0 && ct(this.prototype, t, i);
}
}
static getPropertyDescriptor(t, e, s) {
const { get: i, set: n } = dt(this.prototype, t) ?? { get() {
return this[e];
}, set(o) {
this[e] = o;
} };
return { get: i, set(o) {
const a = i == null ? void 0 : i.call(this);
n == null || n.call(this, o), this.requestUpdate(t, a, s);
}, configurable: !0, enumerable: !0 };
}
static getPropertyOptions(t) {
return this.elementProperties.get(t) ?? J;
}
static _$Ei() {
if (this.hasOwnProperty(w("elementProperties"))) return;
const t = $t(this);
t.finalize(), t.l !== void 0 && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties);
}
static finalize() {
if (this.hasOwnProperty(w("finalized"))) return;
if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(w("properties"))) {
const e = this.properties, s = [...pt(e), ...ut(e)];
for (const i of s) this.createProperty(i, e[i]);
}
const t = this[Symbol.metadata];
if (t !== null) {
const e = litPropertyMetadata.get(t);
if (e !== void 0) for (const [s, i] of e) this.elementProperties.set(s, i);
}
this._$Eh = /* @__PURE__ */ new Map();
for (const [e, s] of this.elementProperties) {
const i = this._$Eu(e, s);
i !== void 0 && this._$Eh.set(i, e);
}
this.elementStyles = this.finalizeStyles(this.styles);
}
static finalizeStyles(t) {
const e = [];
if (Array.isArray(t)) {
const s = new Set(t.flat(1 / 0).reverse());
for (const i of s) e.unshift(V(i));
} else t !== void 0 && e.push(V(t));
return e;
}
static _$Eu(t, e) {
const s = e.attribute;
return s === !1 ? void 0 : typeof s == "string" ? s : typeof t == "string" ? t.toLowerCase() : void 0;
}
constructor() {
super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev();
}
_$Ev() {
var t;
this._$ES = new Promise((e) => this.enableUpdating = e), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (t = this.constructor.l) == null || t.forEach((e) => e(this));
}
addController(t) {
var e;
(this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t), this.renderRoot !== void 0 && this.isConnected && ((e = t.hostConnected) == null || e.call(t));
}
removeController(t) {
var e;
(e = this._$EO) == null || e.delete(t);
}
_$E_() {
const t = /* @__PURE__ */ new Map(), e = this.constructor.elementProperties;
for (const s of e.keys()) this.hasOwnProperty(s) && (t.set(s, this[s]), delete this[s]);
t.size > 0 && (this._$Ep = t);
}
createRenderRoot() {
const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
return at(t, this.constructor.elementStyles), t;
}
connectedCallback() {
var t;
this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(!0), (t = this._$EO) == null || t.forEach((e) => {
var s;
return (s = e.hostConnected) == null ? void 0 : s.call(e);
});
}
enableUpdating(t) {
}
disconnectedCallback() {
var t;
(t = this._$EO) == null || t.forEach((e) => {
var s;
return (s = e.hostDisconnected) == null ? void 0 : s.call(e);
});
}
attributeChangedCallback(t, e, s) {
this._$AK(t, s);
}
_$ET(t, e) {
var n;
const s = this.constructor.elementProperties.get(t), i = this.constructor._$Eu(t, s);
if (i !== void 0 && s.reflect === !0) {
const o = (((n = s.converter) == null ? void 0 : n.toAttribute) !== void 0 ? s.converter : j).toAttribute(e, s.type);
this._$Em = t, o == null ? this.removeAttribute(i) : this.setAttribute(i, o), this._$Em = null;
}
}
_$AK(t, e) {
var n, o;
const s = this.constructor, i = s._$Eh.get(t);
if (i !== void 0 && this._$Em !== i) {
const a = s.getPropertyOptions(i), h = typeof a.converter == "function" ? { fromAttribute: a.converter } : ((n = a.converter) == null ? void 0 : n.fromAttribute) !== void 0 ? a.converter : j;
this._$Em = i, this[i] = h.fromAttribute(e, a.type) ?? ((o = this._$Ej) == null ? void 0 : o.get(i)) ?? null, this._$Em = null;
}
}
requestUpdate(t, e, s) {
var i;
if (t !== void 0) {
const n = this.constructor, o = this[t];
if (s ?? (s = n.getPropertyOptions(t)), !((s.hasChanged ?? et)(o, e) || s.useDefault && s.reflect && o === ((i = this._$Ej) == null ? void 0 : i.get(t)) && !this.hasAttribute(n._$Eu(t, s)))) return;
this.C(t, e, s);
}
this.isUpdatePending === !1 && (this._$ES = this._$EP());
}
C(t, e, { useDefault: s, reflect: i, wrapped: n }, o) {
s && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t) && (this._$Ej.set(t, o ?? e ?? this[t]), n !== !0 || o !== void 0) || (this._$AL.has(t) || (this.hasUpdated || s || (e = void 0), this._$AL.set(t, e)), i === !0 && this._$Em !== t && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t));
}
async _$EP() {
this.isUpdatePending = !0;
try {
await this._$ES;
} catch (e) {
Promise.reject(e);
}
const t = this.scheduleUpdate();
return t != null && await t, !this.isUpdatePending;
}
scheduleUpdate() {
return this.performUpdate();
}
performUpdate() {
var s;
if (!this.isUpdatePending) return;
if (!this.hasUpdated) {
if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
for (const [n, o] of this._$Ep) this[n] = o;
this._$Ep = void 0;
}
const i = this.constructor.elementProperties;
if (i.size > 0) for (const [n, o] of i) {
const { wrapped: a } = o, h = this[n];
a !== !0 || this._$AL.has(n) || h === void 0 || this.C(n, void 0, o, h);
}
}
let t = !1;
const e = this._$AL;
try {
t = this.shouldUpdate(e), t ? (this.willUpdate(e), (s = this._$EO) == null || s.forEach((i) => {
var n;
return (n = i.hostUpdate) == null ? void 0 : n.call(i);
}), this.update(e)) : this._$EM();
} catch (i) {
throw t = !1, this._$EM(), i;
}
t && this._$AE(e);
}
willUpdate(t) {
}
_$AE(t) {
var e;
(e = this._$EO) == null || e.forEach((s) => {
var i;
return (i = s.hostUpdated) == null ? void 0 : i.call(s);
}), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t)), this.updated(t);
}
_$EM() {
this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1;
}
get updateComplete() {
return this.getUpdateComplete();
}
getUpdateComplete() {
return this._$ES;
}
shouldUpdate(t) {
return !0;
}
update(t) {
this._$Eq && (this._$Eq = this._$Eq.forEach((e) => this._$ET(e, this[e]))), this._$EM();
}
updated(t) {
}
firstUpdated(t) {
}
};
v.elementStyles = [], v.shadowRootOptions = { mode: "open" }, v[w("elementProperties")] = /* @__PURE__ */ new Map(), v[w("finalized")] = /* @__PURE__ */ new Map(), k == null || k({ ReactiveElement: v }), (f.reactiveElementVersions ?? (f.reactiveElementVersions = [])).push("2.1.0");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const C = globalThis, N = C.trustedTypes, K = N ? N.createPolicy("lit-html", { createHTML: (r) => r }) : void 0, st = "$lit$", _ = `lit$${Math.random().toFixed(9).slice(2)}$`, it = "?" + _, ft = `<${it}>`, g = document, P = () => g.createComment(""), x = (r) => r === null || typeof r != "object" && typeof r != "function", I = Array.isArray, At = (r) => I(r) || typeof (r == null ? void 0 : r[Symbol.iterator]) == "function", D = `[
\f\r]`, b = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, Z = /-->/g, F = />/g, A = RegExp(`>|${D}(?:([^\\s"'>=/]+)(${D}*=${D}*(?:[^
\f\r"'\`<>=]|("|')|))|$)`, "g"), G = /'/g, Q = /"/g, rt = /^(?:script|style|textarea|title)$/i, mt = (r) => (t, ...e) => ({ _$litType$: r, strings: t, values: e }), xt = mt(1), E = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), X = /* @__PURE__ */ new WeakMap(), m = g.createTreeWalker(g, 129);
function nt(r, t) {
if (!I(r) || !r.hasOwnProperty("raw")) throw Error("invalid template strings array");
return K !== void 0 ? K.createHTML(t) : t;
}
const yt = (r, t) => {
const e = r.length - 1, s = [];
let i, n = t === 2 ? "<svg>" : t === 3 ? "<math>" : "", o = b;
for (let a = 0; a < e; a++) {
const h = r[a];
let c, p, l = -1, u = 0;
for (; u < h.length && (o.lastIndex = u, p = o.exec(h), p !== null); ) u = o.lastIndex, o === b ? p[1] === "!--" ? o = Z : p[1] !== void 0 ? o = F : p[2] !== void 0 ? (rt.test(p[2]) && (i = RegExp("</" + p[2], "g")), o = A) : p[3] !== void 0 && (o = A) : o === A ? p[0] === ">" ? (o = i ?? b, l = -1) : p[1] === void 0 ? l = -2 : (l = o.lastIndex - p[2].length, c = p[1], o = p[3] === void 0 ? A : p[3] === '"' ? Q : G) : o === Q || o === G ? o = A : o === Z || o === F ? o = b : (o = A, i = void 0);
const $ = o === A && r[a + 1].startsWith("/>") ? " " : "";
n += o === b ? h + ft : l >= 0 ? (s.push(c), h.slice(0, l) + st + h.slice(l) + _ + $) : h + _ + (l === -2 ? a : $);
}
return [nt(r, n + (r[e] || "<?>") + (t === 2 ? "</svg>" : t === 3 ? "</math>" : "")), s];
};
class U {
constructor({ strings: t, _$litType$: e }, s) {
let i;
this.parts = [];
let n = 0, o = 0;
const a = t.length - 1, h = this.parts, [c, p] = yt(t, e);
if (this.el = U.createElement(c, s), m.currentNode = this.el.content, e === 2 || e === 3) {
const l = this.el.content.firstChild;
l.replaceWith(...l.childNodes);
}
for (; (i = m.nextNode()) !== null && h.length < a; ) {
if (i.nodeType === 1) {
if (i.hasAttributes()) for (const l of i.getAttributeNames()) if (l.endsWith(st)) {
const u = p[o++], $ = i.getAttribute(l).split(_), H = /([.?@])?(.*)/.exec(u);
h.push({ type: 1, index: n, name: H[2], strings: $, ctor: H[1] === "." ? vt : H[1] === "?" ? Et : H[1] === "@" ? St : R }), i.removeAttribute(l);
} else l.startsWith(_) && (h.push({ type: 6, index: n }), i.removeAttribute(l));
if (rt.test(i.tagName)) {
const l = i.textContent.split(_), u = l.length - 1;
if (u > 0) {
i.textContent = N ? N.emptyScript : "";
for (let $ = 0; $ < u; $++) i.append(l[$], P()), m.nextNode(), h.push({ type: 2, index: ++n });
i.append(l[u], P());
}
}
} else if (i.nodeType === 8) if (i.data === it) h.push({ type: 2, index: n });
else {
let l = -1;
for (; (l = i.data.indexOf(_, l + 1)) !== -1; ) h.push({ type: 7, index: n }), l += _.length - 1;
}
n++;
}
}
static createElement(t, e) {
const s = g.createElement("template");
return s.innerHTML = t, s;
}
}
function S(r, t, e = r, s) {
var o, a;
if (t === E) return t;
let i = s !== void 0 ? (o = e._$Co) == null ? void 0 : o[s] : e._$Cl;
const n = x(t) ? void 0 : t._$litDirective$;
return (i == null ? void 0 : i.constructor) !== n && ((a = i == null ? void 0 : i._$AO) == null || a.call(i, !1), n === void 0 ? i = void 0 : (i = new n(r), i._$AT(r, e, s)), s !== void 0 ? (e._$Co ?? (e._$Co = []))[s] = i : e._$Cl = i), i !== void 0 && (t = S(r, i._$AS(r, t.values), i, s)), t;
}
class gt {
constructor(t, e) {
this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = e;
}
get parentNode() {
return this._$AM.parentNode;
}
get _$AU() {
return this._$AM._$AU;
}
u(t) {
const { el: { content: e }, parts: s } = this._$AD, i = ((t == null ? void 0 : t.creationScope) ?? g).importNode(e, !0);
m.currentNode = i;
let n = m.nextNode(), o = 0, a = 0, h = s[0];
for (; h !== void 0; ) {
if (o === h.index) {
let c;
h.type === 2 ? c = new O(n, n.nextSibling, this, t) : h.type === 1 ? c = new h.ctor(n, h.name, h.strings, this, t) : h.type === 6 && (c = new bt(n, this, t)), this._$AV.push(c), h = s[++a];
}
o !== (h == null ? void 0 : h.index) && (n = m.nextNode(), o++);
}
return m.currentNode = g, i;
}
p(t) {
let e = 0;
for (const s of this._$AV) s !== void 0 && (s.strings !== void 0 ? (s._$AI(t, s, e), e += s.strings.length - 2) : s._$AI(t[e])), e++;
}
}
class O {
get _$AU() {
var t;
return ((t = this._$AM) == null ? void 0 : t._$AU) ?? this._$Cv;
}
constructor(t, e, s, i) {
this.type = 2, this._$AH = d, this._$AN = void 0, this._$AA = t, this._$AB = e, this._$AM = s, this.options = i, this._$Cv = (i == null ? void 0 : i.isConnected) ?? !0;
}
get parentNode() {
let t = this._$AA.parentNode;
const e = this._$AM;
return e !== void 0 && (t == null ? void 0 : t.nodeType) === 11 && (t = e.parentNode), t;
}
get startNode() {
return this._$AA;
}
get endNode() {
return this._$AB;
}
_$AI(t, e = this) {
t = S(this, t, e), x(t) ? t === d || t == null || t === "" ? (this._$AH !== d && this._$AR(), this._$AH = d) : t !== this._$AH && t !== E && this._(t) : t._$litType$ !== void 0 ? this.$(t) : t.nodeType !== void 0 ? this.T(t) : At(t) ? this.k(t) : this._(t);
}
O(t) {
return this._$AA.parentNode.insertBefore(t, this._$AB);
}
T(t) {
this._$AH !== t && (this._$AR(), this._$AH = this.O(t));
}
_(t) {
this._$AH !== d && x(this._$AH) ? this._$AA.nextSibling.data = t : this.T(g.createTextNode(t)), this._$AH = t;
}
$(t) {
var n;
const { values: e, _$litType$: s } = t, i = typeof s == "number" ? this._$AC(t) : (s.el === void 0 && (s.el = U.createElement(nt(s.h, s.h[0]), this.options)), s);
if (((n = this._$AH) == null ? void 0 : n._$AD) === i) this._$AH.p(e);
else {
const o = new gt(i, this), a = o.u(this.options);
o.p(e), this.T(a), this._$AH = o;
}
}
_$AC(t) {
let e = X.get(t.strings);
return e === void 0 && X.set(t.strings, e = new U(t)), e;
}
k(t) {
I(this._$AH) || (this._$AH = [], this._$AR());
const e = this._$AH;
let s, i = 0;
for (const n of t) i === e.length ? e.push(s = new O(this.O(P()), this.O(P()), this, this.options)) : s = e[i], s._$AI(n), i++;
i < e.length && (this._$AR(s && s._$AB.nextSibling, i), e.length = i);
}
_$AR(t = this._$AA.nextSibling, e) {
var s;
for ((s = this._$AP) == null ? void 0 : s.call(this, !1, !0, e); t && t !== this._$AB; ) {
const i = t.nextSibling;
t.remove(), t = i;
}
}
setConnected(t) {
var e;
this._$AM === void 0 && (this._$Cv = t, (e = this._$AP) == null || e.call(this, t));
}
}
class R {
get tagName() {
return this.element.tagName;
}
get _$AU() {
return this._$AM._$AU;
}
constructor(t, e, s, i, n) {
this.type = 1, this._$AH = d, this._$AN = void 0, this.element = t, this.name = e, this._$AM = i, this.options = n, s.length > 2 || s[0] !== "" || s[1] !== "" ? (this._$AH = Array(s.length - 1).fill(new String()), this.strings = s) : this._$AH = d;
}
_$AI(t, e = this, s, i) {
const n = this.strings;
let o = !1;
if (n === void 0) t = S(this, t, e, 0), o = !x(t) || t !== this._$AH && t !== E, o && (this._$AH = t);
else {
const a = t;
let h, c;
for (t = n[0], h = 0; h < n.length - 1; h++) c = S(this, a[s + h], e, h), c === E && (c = this._$AH[h]), o || (o = !x(c) || c !== this._$AH[h]), c === d ? t = d : t !== d && (t += (c ?? "") + n[h + 1]), this._$AH[h] = c;
}
o && !i && this.j(t);
}
j(t) {
t === d ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? "");
}
}
class vt extends R {
constructor() {
super(...arguments), this.type = 3;
}
j(t) {
this.element[this.name] = t === d ? void 0 : t;
}
}
class Et extends R {
constructor() {
super(...arguments), this.type = 4;
}
j(t) {
this.element.toggleAttribute(this.name, !!t && t !== d);
}
}
class St extends R {
constructor(t, e, s, i, n) {
super(t, e, s, i, n), this.type = 5;
}
_$AI(t, e = this) {
if ((t = S(this, t, e, 0) ?? d) === E) return;
const s = this._$AH, i = t === d && s !== d || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, n = t !== d && (s === d || i);
i && this.element.removeEventListener(this.name, this, s), n && this.element.addEventListener(this.name, this, t), this._$AH = t;
}
handleEvent(t) {
var e;
typeof this._$AH == "function" ? this._$AH.call(((e = this.options) == null ? void 0 : e.host) ?? this.element, t) : this._$AH.handleEvent(t);
}
}
class bt {
constructor(t, e, s) {
this.element = t, this.type = 6, this._$AN = void 0, this._$AM = e, this.options = s;
}
get _$AU() {
return this._$AM._$AU;
}
_$AI(t) {
S(this, t);
}
}
const L = C.litHtmlPolyfillSupport;
L == null || L(U, O), (C.litHtmlVersions ?? (C.litHtmlVersions = [])).push("3.3.0");
const wt = (r, t, e) => {
const s = (e == null ? void 0 : e.renderBefore) ?? t;
let i = s._$litPart$;
if (i === void 0) {
const n = (e == null ? void 0 : e.renderBefore) ?? null;
s._$litPart$ = i = new O(t.insertBefore(P(), n), n, void 0, e ?? {});
}
return i._$AI(r), i;
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const y = globalThis;
class T extends v {
constructor() {
super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
}
createRenderRoot() {
var e;
const t = super.createRenderRoot();
return (e = this.renderOptions).renderBefore ?? (e.renderBefore = t.firstChild), t;
}
update(t) {
const e = this.render();
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = wt(e, this.renderRoot, this.renderOptions);
}
connectedCallback() {
var t;
super.connectedCallback(), (t = this._$Do) == null || t.setConnected(!0);
}
disconnectedCallback() {
var t;
super.disconnectedCallback(), (t = this._$Do) == null || t.setConnected(!1);
}
render() {
return E;
}
}
var Y;
T._$litElement$ = !0, T.finalized = !0, (Y = y.litElementHydrateSupport) == null || Y.call(y, { LitElement: T });
const z = y.litElementPolyfillSupport;
z == null || z({ LitElement: T });
(y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0");
export {
d as E,
et as f,
T as i,
j as u,
xt as x
};

View File

@@ -0,0 +1,550 @@
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
var _a;
const t$1 = globalThis, e$2 = t$1.ShadowRoot && (void 0 === t$1.ShadyCSS || t$1.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$2 = Symbol(), o$3 = /* @__PURE__ */ new WeakMap();
let n$2 = class n {
constructor(t2, e2, o2) {
if (this._$cssResult$ = true, o2 !== s$2) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
this.cssText = t2, this.t = e2;
}
get styleSheet() {
let t2 = this.o;
const s2 = this.t;
if (e$2 && void 0 === t2) {
const e2 = void 0 !== s2 && 1 === s2.length;
e2 && (t2 = o$3.get(s2)), void 0 === t2 && ((this.o = t2 = new CSSStyleSheet()).replaceSync(this.cssText), e2 && o$3.set(s2, t2));
}
return t2;
}
toString() {
return this.cssText;
}
};
const r$2 = (t2) => new n$2("string" == typeof t2 ? t2 : t2 + "", void 0, s$2), S$1 = (s2, o2) => {
if (e$2) s2.adoptedStyleSheets = o2.map((t2) => t2 instanceof CSSStyleSheet ? t2 : t2.styleSheet);
else for (const e2 of o2) {
const o3 = document.createElement("style"), n3 = t$1.litNonce;
void 0 !== n3 && o3.setAttribute("nonce", n3), o3.textContent = e2.cssText, s2.appendChild(o3);
}
}, c$2 = e$2 ? (t2) => t2 : (t2) => t2 instanceof CSSStyleSheet ? ((t3) => {
let e2 = "";
for (const s2 of t3.cssRules) e2 += s2.cssText;
return r$2(e2);
})(t2) : t2;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const { is: i$2, defineProperty: e$1, getOwnPropertyDescriptor: h$1, getOwnPropertyNames: r$1, getOwnPropertySymbols: o$2, getPrototypeOf: n$1 } = Object, a$1 = globalThis, c$1 = a$1.trustedTypes, l$1 = c$1 ? c$1.emptyScript : "", p$1 = a$1.reactiveElementPolyfillSupport, d$1 = (t2, s2) => t2, u$1 = { toAttribute(t2, s2) {
switch (s2) {
case Boolean:
t2 = t2 ? l$1 : null;
break;
case Object:
case Array:
t2 = null == t2 ? t2 : JSON.stringify(t2);
}
return t2;
}, fromAttribute(t2, s2) {
let i2 = t2;
switch (s2) {
case Boolean:
i2 = null !== t2;
break;
case Number:
i2 = null === t2 ? null : Number(t2);
break;
case Object:
case Array:
try {
i2 = JSON.parse(t2);
} catch (t3) {
i2 = null;
}
}
return i2;
} }, f$1 = (t2, s2) => !i$2(t2, s2), b = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 };
Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), a$1.litPropertyMetadata ?? (a$1.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
let y$1 = class y extends HTMLElement {
static addInitializer(t2) {
this._$Ei(), (this.l ?? (this.l = [])).push(t2);
}
static get observedAttributes() {
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
}
static createProperty(t2, s2 = b) {
if (s2.state && (s2.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t2) && ((s2 = Object.create(s2)).wrapped = true), this.elementProperties.set(t2, s2), !s2.noAccessor) {
const i2 = Symbol(), h2 = this.getPropertyDescriptor(t2, i2, s2);
void 0 !== h2 && e$1(this.prototype, t2, h2);
}
}
static getPropertyDescriptor(t2, s2, i2) {
const { get: e2, set: r2 } = h$1(this.prototype, t2) ?? { get() {
return this[s2];
}, set(t3) {
this[s2] = t3;
} };
return { get: e2, set(s3) {
const h2 = e2 == null ? void 0 : e2.call(this);
r2 == null ? void 0 : r2.call(this, s3), this.requestUpdate(t2, h2, i2);
}, configurable: true, enumerable: true };
}
static getPropertyOptions(t2) {
return this.elementProperties.get(t2) ?? b;
}
static _$Ei() {
if (this.hasOwnProperty(d$1("elementProperties"))) return;
const t2 = n$1(this);
t2.finalize(), void 0 !== t2.l && (this.l = [...t2.l]), this.elementProperties = new Map(t2.elementProperties);
}
static finalize() {
if (this.hasOwnProperty(d$1("finalized"))) return;
if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d$1("properties"))) {
const t3 = this.properties, s2 = [...r$1(t3), ...o$2(t3)];
for (const i2 of s2) this.createProperty(i2, t3[i2]);
}
const t2 = this[Symbol.metadata];
if (null !== t2) {
const s2 = litPropertyMetadata.get(t2);
if (void 0 !== s2) for (const [t3, i2] of s2) this.elementProperties.set(t3, i2);
}
this._$Eh = /* @__PURE__ */ new Map();
for (const [t3, s2] of this.elementProperties) {
const i2 = this._$Eu(t3, s2);
void 0 !== i2 && this._$Eh.set(i2, t3);
}
this.elementStyles = this.finalizeStyles(this.styles);
}
static finalizeStyles(s2) {
const i2 = [];
if (Array.isArray(s2)) {
const e2 = new Set(s2.flat(1 / 0).reverse());
for (const s3 of e2) i2.unshift(c$2(s3));
} else void 0 !== s2 && i2.push(c$2(s2));
return i2;
}
static _$Eu(t2, s2) {
const i2 = s2.attribute;
return false === i2 ? void 0 : "string" == typeof i2 ? i2 : "string" == typeof t2 ? t2.toLowerCase() : void 0;
}
constructor() {
super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();
}
_$Ev() {
var _a2;
this._$ES = new Promise((t2) => this.enableUpdating = t2), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (_a2 = this.constructor.l) == null ? void 0 : _a2.forEach((t2) => t2(this));
}
addController(t2) {
var _a2;
(this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t2), void 0 !== this.renderRoot && this.isConnected && ((_a2 = t2.hostConnected) == null ? void 0 : _a2.call(t2));
}
removeController(t2) {
var _a2;
(_a2 = this._$EO) == null ? void 0 : _a2.delete(t2);
}
_$E_() {
const t2 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;
for (const i2 of s2.keys()) this.hasOwnProperty(i2) && (t2.set(i2, this[i2]), delete this[i2]);
t2.size > 0 && (this._$Ep = t2);
}
createRenderRoot() {
const t2 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
return S$1(t2, this.constructor.elementStyles), t2;
}
connectedCallback() {
var _a2;
this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(true), (_a2 = this._$EO) == null ? void 0 : _a2.forEach((t2) => {
var _a3;
return (_a3 = t2.hostConnected) == null ? void 0 : _a3.call(t2);
});
}
enableUpdating(t2) {
}
disconnectedCallback() {
var _a2;
(_a2 = this._$EO) == null ? void 0 : _a2.forEach((t2) => {
var _a3;
return (_a3 = t2.hostDisconnected) == null ? void 0 : _a3.call(t2);
});
}
attributeChangedCallback(t2, s2, i2) {
this._$AK(t2, i2);
}
_$ET(t2, s2) {
var _a2;
const i2 = this.constructor.elementProperties.get(t2), e2 = this.constructor._$Eu(t2, i2);
if (void 0 !== e2 && true === i2.reflect) {
const h2 = (void 0 !== ((_a2 = i2.converter) == null ? void 0 : _a2.toAttribute) ? i2.converter : u$1).toAttribute(s2, i2.type);
this._$Em = t2, null == h2 ? this.removeAttribute(e2) : this.setAttribute(e2, h2), this._$Em = null;
}
}
_$AK(t2, s2) {
var _a2, _b;
const i2 = this.constructor, e2 = i2._$Eh.get(t2);
if (void 0 !== e2 && this._$Em !== e2) {
const t3 = i2.getPropertyOptions(e2), h2 = "function" == typeof t3.converter ? { fromAttribute: t3.converter } : void 0 !== ((_a2 = t3.converter) == null ? void 0 : _a2.fromAttribute) ? t3.converter : u$1;
this._$Em = e2, this[e2] = h2.fromAttribute(s2, t3.type) ?? ((_b = this._$Ej) == null ? void 0 : _b.get(e2)) ?? null, this._$Em = null;
}
}
requestUpdate(t2, s2, i2) {
var _a2;
if (void 0 !== t2) {
const e2 = this.constructor, h2 = this[t2];
if (i2 ?? (i2 = e2.getPropertyOptions(t2)), !((i2.hasChanged ?? f$1)(h2, s2) || i2.useDefault && i2.reflect && h2 === ((_a2 = this._$Ej) == null ? void 0 : _a2.get(t2)) && !this.hasAttribute(e2._$Eu(t2, i2)))) return;
this.C(t2, s2, i2);
}
false === this.isUpdatePending && (this._$ES = this._$EP());
}
C(t2, s2, { useDefault: i2, reflect: e2, wrapped: h2 }, r2) {
i2 && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t2) && (this._$Ej.set(t2, r2 ?? s2 ?? this[t2]), true !== h2 || void 0 !== r2) || (this._$AL.has(t2) || (this.hasUpdated || i2 || (s2 = void 0), this._$AL.set(t2, s2)), true === e2 && this._$Em !== t2 && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t2));
}
async _$EP() {
this.isUpdatePending = true;
try {
await this._$ES;
} catch (t3) {
Promise.reject(t3);
}
const t2 = this.scheduleUpdate();
return null != t2 && await t2, !this.isUpdatePending;
}
scheduleUpdate() {
return this.performUpdate();
}
performUpdate() {
var _a2;
if (!this.isUpdatePending) return;
if (!this.hasUpdated) {
if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
for (const [t4, s3] of this._$Ep) this[t4] = s3;
this._$Ep = void 0;
}
const t3 = this.constructor.elementProperties;
if (t3.size > 0) for (const [s3, i2] of t3) {
const { wrapped: t4 } = i2, e2 = this[s3];
true !== t4 || this._$AL.has(s3) || void 0 === e2 || this.C(s3, void 0, i2, e2);
}
}
let t2 = false;
const s2 = this._$AL;
try {
t2 = this.shouldUpdate(s2), t2 ? (this.willUpdate(s2), (_a2 = this._$EO) == null ? void 0 : _a2.forEach((t3) => {
var _a3;
return (_a3 = t3.hostUpdate) == null ? void 0 : _a3.call(t3);
}), this.update(s2)) : this._$EM();
} catch (s3) {
throw t2 = false, this._$EM(), s3;
}
t2 && this._$AE(s2);
}
willUpdate(t2) {
}
_$AE(t2) {
var _a2;
(_a2 = this._$EO) == null ? void 0 : _a2.forEach((t3) => {
var _a3;
return (_a3 = t3.hostUpdated) == null ? void 0 : _a3.call(t3);
}), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t2)), this.updated(t2);
}
_$EM() {
this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;
}
get updateComplete() {
return this.getUpdateComplete();
}
getUpdateComplete() {
return this._$ES;
}
shouldUpdate(t2) {
return true;
}
update(t2) {
this._$Eq && (this._$Eq = this._$Eq.forEach((t3) => this._$ET(t3, this[t3]))), this._$EM();
}
updated(t2) {
}
firstUpdated(t2) {
}
};
y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$1("elementProperties")] = /* @__PURE__ */ new Map(), y$1[d$1("finalized")] = /* @__PURE__ */ new Map(), p$1 == null ? void 0 : p$1({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ?? (a$1.reactiveElementVersions = [])).push("2.1.0");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t = globalThis, i$1 = t.trustedTypes, s$1 = i$1 ? i$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e = "$lit$", h = `lit$${Math.random().toFixed(9).slice(2)}$`, o$1 = "?" + h, n2 = `<${o$1}>`, r = document, l = () => r.createComment(""), c = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u = (t2) => a(t2) || "function" == typeof (t2 == null ? void 0 : t2[Symbol.iterator]), d = "[ \n\f\r]", f = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v = /-->/g, _ = />/g, m = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), x = y2(1), T = Symbol.for("lit-noChange"), E = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r.createTreeWalker(r, 129);
function P(t2, i2) {
if (!a(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array");
return void 0 !== s$1 ? s$1.createHTML(i2) : i2;
}
const V = (t2, i2) => {
const s2 = t2.length - 1, o2 = [];
let r2, l2 = 2 === i2 ? "<svg>" : 3 === i2 ? "<math>" : "", c2 = f;
for (let i3 = 0; i3 < s2; i3++) {
const s3 = t2[i3];
let a2, u2, d2 = -1, y3 = 0;
for (; y3 < s3.length && (c2.lastIndex = y3, u2 = c2.exec(s3), null !== u2); ) y3 = c2.lastIndex, c2 === f ? "!--" === u2[1] ? c2 = v : void 0 !== u2[1] ? c2 = _ : void 0 !== u2[2] ? ($.test(u2[2]) && (r2 = RegExp("</" + u2[2], "g")), c2 = m) : void 0 !== u2[3] && (c2 = m) : c2 === m ? ">" === u2[0] ? (c2 = r2 ?? f, d2 = -1) : void 0 === u2[1] ? d2 = -2 : (d2 = c2.lastIndex - u2[2].length, a2 = u2[1], c2 = void 0 === u2[3] ? m : '"' === u2[3] ? g : p) : c2 === g || c2 === p ? c2 = m : c2 === v || c2 === _ ? c2 = f : (c2 = m, r2 = void 0);
const x2 = c2 === m && t2[i3 + 1].startsWith("/>") ? " " : "";
l2 += c2 === f ? s3 + n2 : d2 >= 0 ? (o2.push(a2), s3.slice(0, d2) + e + s3.slice(d2) + h + x2) : s3 + h + (-2 === d2 ? i3 : x2);
}
return [P(t2, l2 + (t2[s2] || "<?>") + (2 === i2 ? "</svg>" : 3 === i2 ? "</math>" : "")), o2];
};
class N {
constructor({ strings: t2, _$litType$: s2 }, n3) {
let r2;
this.parts = [];
let c2 = 0, a2 = 0;
const u2 = t2.length - 1, d2 = this.parts, [f2, v2] = V(t2, s2);
if (this.el = N.createElement(f2, n3), C.currentNode = this.el.content, 2 === s2 || 3 === s2) {
const t3 = this.el.content.firstChild;
t3.replaceWith(...t3.childNodes);
}
for (; null !== (r2 = C.nextNode()) && d2.length < u2; ) {
if (1 === r2.nodeType) {
if (r2.hasAttributes()) for (const t3 of r2.getAttributeNames()) if (t3.endsWith(e)) {
const i2 = v2[a2++], s3 = r2.getAttribute(t3).split(h), e2 = /([.?@])?(.*)/.exec(i2);
d2.push({ type: 1, index: c2, name: e2[2], strings: s3, ctor: "." === e2[1] ? H : "?" === e2[1] ? I : "@" === e2[1] ? L : k }), r2.removeAttribute(t3);
} else t3.startsWith(h) && (d2.push({ type: 6, index: c2 }), r2.removeAttribute(t3));
if ($.test(r2.tagName)) {
const t3 = r2.textContent.split(h), s3 = t3.length - 1;
if (s3 > 0) {
r2.textContent = i$1 ? i$1.emptyScript : "";
for (let i2 = 0; i2 < s3; i2++) r2.append(t3[i2], l()), C.nextNode(), d2.push({ type: 2, index: ++c2 });
r2.append(t3[s3], l());
}
}
} else if (8 === r2.nodeType) if (r2.data === o$1) d2.push({ type: 2, index: c2 });
else {
let t3 = -1;
for (; -1 !== (t3 = r2.data.indexOf(h, t3 + 1)); ) d2.push({ type: 7, index: c2 }), t3 += h.length - 1;
}
c2++;
}
}
static createElement(t2, i2) {
const s2 = r.createElement("template");
return s2.innerHTML = t2, s2;
}
}
function S(t2, i2, s2 = t2, e2) {
var _a2, _b;
if (i2 === T) return i2;
let h2 = void 0 !== e2 ? (_a2 = s2._$Co) == null ? void 0 : _a2[e2] : s2._$Cl;
const o2 = c(i2) ? void 0 : i2._$litDirective$;
return (h2 == null ? void 0 : h2.constructor) !== o2 && ((_b = h2 == null ? void 0 : h2._$AO) == null ? void 0 : _b.call(h2, false), void 0 === o2 ? h2 = void 0 : (h2 = new o2(t2), h2._$AT(t2, s2, e2)), void 0 !== e2 ? (s2._$Co ?? (s2._$Co = []))[e2] = h2 : s2._$Cl = h2), void 0 !== h2 && (i2 = S(t2, h2._$AS(t2, i2.values), h2, e2)), i2;
}
class M {
constructor(t2, i2) {
this._$AV = [], this._$AN = void 0, this._$AD = t2, this._$AM = i2;
}
get parentNode() {
return this._$AM.parentNode;
}
get _$AU() {
return this._$AM._$AU;
}
u(t2) {
const { el: { content: i2 }, parts: s2 } = this._$AD, e2 = ((t2 == null ? void 0 : t2.creationScope) ?? r).importNode(i2, true);
C.currentNode = e2;
let h2 = C.nextNode(), o2 = 0, n3 = 0, l2 = s2[0];
for (; void 0 !== l2; ) {
if (o2 === l2.index) {
let i3;
2 === l2.type ? i3 = new R(h2, h2.nextSibling, this, t2) : 1 === l2.type ? i3 = new l2.ctor(h2, l2.name, l2.strings, this, t2) : 6 === l2.type && (i3 = new z(h2, this, t2)), this._$AV.push(i3), l2 = s2[++n3];
}
o2 !== (l2 == null ? void 0 : l2.index) && (h2 = C.nextNode(), o2++);
}
return C.currentNode = r, e2;
}
p(t2) {
let i2 = 0;
for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t2, s2, i2), i2 += s2.strings.length - 2) : s2._$AI(t2[i2])), i2++;
}
}
class R {
get _$AU() {
var _a2;
return ((_a2 = this._$AM) == null ? void 0 : _a2._$AU) ?? this._$Cv;
}
constructor(t2, i2, s2, e2) {
this.type = 2, this._$AH = E, this._$AN = void 0, this._$AA = t2, this._$AB = i2, this._$AM = s2, this.options = e2, this._$Cv = (e2 == null ? void 0 : e2.isConnected) ?? true;
}
get parentNode() {
let t2 = this._$AA.parentNode;
const i2 = this._$AM;
return void 0 !== i2 && 11 === (t2 == null ? void 0 : t2.nodeType) && (t2 = i2.parentNode), t2;
}
get startNode() {
return this._$AA;
}
get endNode() {
return this._$AB;
}
_$AI(t2, i2 = this) {
t2 = S(this, t2, i2), c(t2) ? t2 === E || null == t2 || "" === t2 ? (this._$AH !== E && this._$AR(), this._$AH = E) : t2 !== this._$AH && t2 !== T && this._(t2) : void 0 !== t2._$litType$ ? this.$(t2) : void 0 !== t2.nodeType ? this.T(t2) : u(t2) ? this.k(t2) : this._(t2);
}
O(t2) {
return this._$AA.parentNode.insertBefore(t2, this._$AB);
}
T(t2) {
this._$AH !== t2 && (this._$AR(), this._$AH = this.O(t2));
}
_(t2) {
this._$AH !== E && c(this._$AH) ? this._$AA.nextSibling.data = t2 : this.T(r.createTextNode(t2)), this._$AH = t2;
}
$(t2) {
var _a2;
const { values: i2, _$litType$: s2 } = t2, e2 = "number" == typeof s2 ? this._$AC(t2) : (void 0 === s2.el && (s2.el = N.createElement(P(s2.h, s2.h[0]), this.options)), s2);
if (((_a2 = this._$AH) == null ? void 0 : _a2._$AD) === e2) this._$AH.p(i2);
else {
const t3 = new M(e2, this), s3 = t3.u(this.options);
t3.p(i2), this.T(s3), this._$AH = t3;
}
}
_$AC(t2) {
let i2 = A.get(t2.strings);
return void 0 === i2 && A.set(t2.strings, i2 = new N(t2)), i2;
}
k(t2) {
a(this._$AH) || (this._$AH = [], this._$AR());
const i2 = this._$AH;
let s2, e2 = 0;
for (const h2 of t2) e2 === i2.length ? i2.push(s2 = new R(this.O(l()), this.O(l()), this, this.options)) : s2 = i2[e2], s2._$AI(h2), e2++;
e2 < i2.length && (this._$AR(s2 && s2._$AB.nextSibling, e2), i2.length = e2);
}
_$AR(t2 = this._$AA.nextSibling, i2) {
var _a2;
for ((_a2 = this._$AP) == null ? void 0 : _a2.call(this, false, true, i2); t2 && t2 !== this._$AB; ) {
const i3 = t2.nextSibling;
t2.remove(), t2 = i3;
}
}
setConnected(t2) {
var _a2;
void 0 === this._$AM && (this._$Cv = t2, (_a2 = this._$AP) == null ? void 0 : _a2.call(this, t2));
}
}
class k {
get tagName() {
return this.element.tagName;
}
get _$AU() {
return this._$AM._$AU;
}
constructor(t2, i2, s2, e2, h2) {
this.type = 1, this._$AH = E, this._$AN = void 0, this.element = t2, this.name = i2, this._$AM = e2, this.options = h2, s2.length > 2 || "" !== s2[0] || "" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = E;
}
_$AI(t2, i2 = this, s2, e2) {
const h2 = this.strings;
let o2 = false;
if (void 0 === h2) t2 = S(this, t2, i2, 0), o2 = !c(t2) || t2 !== this._$AH && t2 !== T, o2 && (this._$AH = t2);
else {
const e3 = t2;
let n3, r2;
for (t2 = h2[0], n3 = 0; n3 < h2.length - 1; n3++) r2 = S(this, e3[s2 + n3], i2, n3), r2 === T && (r2 = this._$AH[n3]), o2 || (o2 = !c(r2) || r2 !== this._$AH[n3]), r2 === E ? t2 = E : t2 !== E && (t2 += (r2 ?? "") + h2[n3 + 1]), this._$AH[n3] = r2;
}
o2 && !e2 && this.j(t2);
}
j(t2) {
t2 === E ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t2 ?? "");
}
}
class H extends k {
constructor() {
super(...arguments), this.type = 3;
}
j(t2) {
this.element[this.name] = t2 === E ? void 0 : t2;
}
}
class I extends k {
constructor() {
super(...arguments), this.type = 4;
}
j(t2) {
this.element.toggleAttribute(this.name, !!t2 && t2 !== E);
}
}
class L extends k {
constructor(t2, i2, s2, e2, h2) {
super(t2, i2, s2, e2, h2), this.type = 5;
}
_$AI(t2, i2 = this) {
if ((t2 = S(this, t2, i2, 0) ?? E) === T) return;
const s2 = this._$AH, e2 = t2 === E && s2 !== E || t2.capture !== s2.capture || t2.once !== s2.once || t2.passive !== s2.passive, h2 = t2 !== E && (s2 === E || e2);
e2 && this.element.removeEventListener(this.name, this, s2), h2 && this.element.addEventListener(this.name, this, t2), this._$AH = t2;
}
handleEvent(t2) {
var _a2;
"function" == typeof this._$AH ? this._$AH.call(((_a2 = this.options) == null ? void 0 : _a2.host) ?? this.element, t2) : this._$AH.handleEvent(t2);
}
}
class z {
constructor(t2, i2, s2) {
this.element = t2, this.type = 6, this._$AN = void 0, this._$AM = i2, this.options = s2;
}
get _$AU() {
return this._$AM._$AU;
}
_$AI(t2) {
S(this, t2);
}
}
const j = t.litHtmlPolyfillSupport;
j == null ? void 0 : j(N, R), (t.litHtmlVersions ?? (t.litHtmlVersions = [])).push("3.3.0");
const B = (t2, i2, s2) => {
const e2 = (s2 == null ? void 0 : s2.renderBefore) ?? i2;
let h2 = e2._$litPart$;
if (void 0 === h2) {
const t3 = (s2 == null ? void 0 : s2.renderBefore) ?? null;
e2._$litPart$ = h2 = new R(i2.insertBefore(l(), t3), t3, void 0, s2 ?? {});
}
return h2._$AI(t2), h2;
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const s = globalThis;
class i extends y$1 {
constructor() {
super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
}
createRenderRoot() {
var _a2;
const t2 = super.createRenderRoot();
return (_a2 = this.renderOptions).renderBefore ?? (_a2.renderBefore = t2.firstChild), t2;
}
update(t2) {
const r2 = this.render();
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t2), this._$Do = B(r2, this.renderRoot, this.renderOptions);
}
connectedCallback() {
var _a2;
super.connectedCallback(), (_a2 = this._$Do) == null ? void 0 : _a2.setConnected(true);
}
disconnectedCallback() {
var _a2;
super.disconnectedCallback(), (_a2 = this._$Do) == null ? void 0 : _a2.setConnected(false);
}
render() {
return T;
}
}
i._$litElement$ = true, i["finalized"] = true, (_a = s.litElementHydrateSupport) == null ? void 0 : _a.call(s, { LitElement: i });
const o = s.litElementPolyfillSupport;
o == null ? void 0 : o({ LitElement: i });
(s.litElementVersions ?? (s.litElementVersions = [])).push("4.2.0");
export {
E,
f$1 as f,
i,
u$1 as u,
x
};

View File

@@ -0,0 +1,47 @@
import { f, u } from "./lit-z6_uA4GX.mjs";
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t = (t2) => (e, o2) => {
void 0 !== o2 ? o2.addInitializer(() => {
customElements.define(t2, e);
}) : customElements.define(t2, e);
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const o = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f }, r = (t2 = o, e, r2) => {
const { kind: n2, metadata: i } = r2;
let s = globalThis.litPropertyMetadata.get(i);
if (void 0 === s && globalThis.litPropertyMetadata.set(i, s = /* @__PURE__ */ new Map()), "setter" === n2 && ((t2 = Object.create(t2)).wrapped = true), s.set(r2.name, t2), "accessor" === n2) {
const { name: o2 } = r2;
return { set(r3) {
const n3 = e.get.call(this);
e.set.call(this, r3), this.requestUpdate(o2, n3, t2);
}, init(e2) {
return void 0 !== e2 && this.C(o2, void 0, t2, e2), e2;
} };
}
if ("setter" === n2) {
const { name: o2 } = r2;
return function(r3) {
const n3 = this[o2];
e.call(this, r3), this.requestUpdate(o2, n3, t2);
};
}
throw Error("Unsupported decorator location: " + n2);
};
function n(t2) {
return (e, o2) => "object" == typeof o2 ? r(t2, e, o2) : ((t3, e2, o3) => {
const r2 = e2.hasOwnProperty(o3);
return e2.constructor.createProperty(o3, t3), r2 ? Object.getOwnPropertyDescriptor(e2, o3) : void 0;
})(t2, e, o2);
}
export {
n,
t
};

View File

@@ -0,0 +1,128 @@
import { E } from "./lit-z6_uA4GX.mjs";
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const f$1 = (o2) => void 0 === o2.strings;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t = { CHILD: 2 }, e$1 = (t2) => (...e2) => ({ _$litDirective$: t2, values: e2 });
class i {
constructor(t2) {
}
get _$AU() {
return this._$AM._$AU;
}
_$AT(t2, e2, i2) {
this._$Ct = t2, this._$AM = e2, this._$Ci = i2;
}
_$AS(t2, e2) {
return this.update(t2, e2);
}
update(t2, e2) {
return this.render(...e2);
}
}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const s = (i2, t2) => {
var _a;
const e2 = i2._$AN;
if (void 0 === e2) return false;
for (const i3 of e2) (_a = i3._$AO) == null ? void 0 : _a.call(i3, t2, false), s(i3, t2);
return true;
}, o$1 = (i2) => {
let t2, e2;
do {
if (void 0 === (t2 = i2._$AM)) break;
e2 = t2._$AN, e2.delete(i2), i2 = t2;
} while (0 === (e2 == null ? void 0 : e2.size));
}, r = (i2) => {
for (let t2; t2 = i2._$AM; i2 = t2) {
let e2 = t2._$AN;
if (void 0 === e2) t2._$AN = e2 = /* @__PURE__ */ new Set();
else if (e2.has(i2)) break;
e2.add(i2), c(t2);
}
};
function h$1(i2) {
void 0 !== this._$AN ? (o$1(this), this._$AM = i2, r(this)) : this._$AM = i2;
}
function n$1(i2, t2 = false, e2 = 0) {
const r2 = this._$AH, h2 = this._$AN;
if (void 0 !== h2 && 0 !== h2.size) if (t2) if (Array.isArray(r2)) for (let i3 = e2; i3 < r2.length; i3++) s(r2[i3], false), o$1(r2[i3]);
else null != r2 && (s(r2, false), o$1(r2));
else s(this, i2);
}
const c = (i2) => {
i2.type == t.CHILD && (i2._$AP ?? (i2._$AP = n$1), i2._$AQ ?? (i2._$AQ = h$1));
};
class f extends i {
constructor() {
super(...arguments), this._$AN = void 0;
}
_$AT(i2, t2, e2) {
super._$AT(i2, t2, e2), r(this), this.isConnected = i2._$AU;
}
_$AO(i2, t2 = true) {
var _a, _b;
i2 !== this.isConnected && (this.isConnected = i2, i2 ? (_a = this.reconnected) == null ? void 0 : _a.call(this) : (_b = this.disconnected) == null ? void 0 : _b.call(this)), t2 && (s(this, i2), o$1(this));
}
setValue(t2) {
if (f$1(this._$Ct)) this._$Ct._$AI(t2, this);
else {
const i2 = [...this._$Ct._$AH];
i2[this._$Ci] = t2, this._$Ct._$AI(i2, this, 0);
}
}
disconnected() {
}
reconnected() {
}
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const e = () => new h();
class h {
}
const o = /* @__PURE__ */ new WeakMap(), n = e$1(class extends f {
render(i2) {
return E;
}
update(i2, [s2]) {
var _a;
const e2 = s2 !== this.G;
return e2 && void 0 !== this.G && this.rt(void 0), (e2 || this.lt !== this.ct) && (this.G = s2, this.ht = (_a = i2.options) == null ? void 0 : _a.host, this.rt(this.ct = i2.element)), E;
}
rt(t2) {
if (this.isConnected || (t2 = void 0), "function" == typeof this.G) {
const i2 = this.ht ?? globalThis;
let s2 = o.get(i2);
void 0 === s2 && (s2 = /* @__PURE__ */ new WeakMap(), o.set(i2, s2)), void 0 !== s2.get(this.G) && this.G.call(this.ht, void 0), s2.set(this.G, t2), void 0 !== t2 && this.G.call(this.ht, t2);
} else this.G.value = t2;
}
get lt() {
var _a, _b;
return "function" == typeof this.G ? (_a = o.get(this.ht ?? globalThis)) == null ? void 0 : _a.get(this.G) : (_b = this.G) == null ? void 0 : _b.value;
}
disconnected() {
this.lt === this.ct && this.rt(void 0);
}
reconnected() {
this.rt(this.ct);
}
});
export {
e,
n
};

View File

@@ -1,172 +0,0 @@
import { f, u as _, E as $ } from "./lit-CWlWuEHk.mjs";
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const T = (t) => (e, s) => {
s !== void 0 ? s.addInitializer(() => {
customElements.define(t, e);
}) : customElements.define(t, e);
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const A = { attribute: !0, type: String, converter: _, reflect: !1, hasChanged: f }, p = (t = A, e, s) => {
const { kind: i, metadata: n } = s;
let r = globalThis.litPropertyMetadata.get(n);
if (r === void 0 && globalThis.litPropertyMetadata.set(n, r = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), r.set(s.name, t), i === "accessor") {
const { name: o } = s;
return { set(h) {
const l = e.get.call(this);
e.set.call(this, h), this.requestUpdate(o, l, t);
}, init(h) {
return h !== void 0 && this.C(o, void 0, t, h), h;
} };
}
if (i === "setter") {
const { name: o } = s;
return function(h) {
const l = this[o];
e.call(this, h), this.requestUpdate(o, l, t);
};
}
throw Error("Unsupported decorator location: " + i);
};
function O(t) {
return (e, s) => typeof s == "object" ? p(t, e, s) : ((i, n, r) => {
const o = n.hasOwnProperty(r);
return n.constructor.createProperty(r, i), o ? Object.getOwnPropertyDescriptor(n, r) : void 0;
})(t, e, s);
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const v = (t) => t.strings === void 0;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const g = { CHILD: 2 }, C = (t) => (...e) => ({ _$litDirective$: t, values: e });
class m {
constructor(e) {
}
get _$AU() {
return this._$AM._$AU;
}
_$AT(e, s, i) {
this._$Ct = e, this._$AM = s, this._$Ci = i;
}
_$AS(e, s) {
return this.update(e, s);
}
update(e, s) {
return this.render(...s);
}
}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const c = (t, e) => {
var i;
const s = t._$AN;
if (s === void 0) return !1;
for (const n of s) (i = n._$AO) == null || i.call(n, e, !1), c(n, e);
return !0;
}, a = (t) => {
let e, s;
do {
if ((e = t._$AM) === void 0) break;
s = e._$AN, s.delete(t), t = e;
} while ((s == null ? void 0 : s.size) === 0);
}, u = (t) => {
for (let e; e = t._$AM; t = e) {
let s = e._$AN;
if (s === void 0) e._$AN = s = /* @__PURE__ */ new Set();
else if (s.has(t)) break;
s.add(t), M(e);
}
};
function y(t) {
this._$AN !== void 0 ? (a(this), this._$AM = t, u(this)) : this._$AM = t;
}
function G(t, e = !1, s = 0) {
const i = this._$AH, n = this._$AN;
if (n !== void 0 && n.size !== 0) if (e) if (Array.isArray(i)) for (let r = s; r < i.length; r++) c(i[r], !1), a(i[r]);
else i != null && (c(i, !1), a(i));
else c(this, t);
}
const M = (t) => {
t.type == g.CHILD && (t._$AP ?? (t._$AP = G), t._$AQ ?? (t._$AQ = y));
};
class b extends m {
constructor() {
super(...arguments), this._$AN = void 0;
}
_$AT(e, s, i) {
super._$AT(e, s, i), u(this), this.isConnected = e._$AU;
}
_$AO(e, s = !0) {
var i, n;
e !== this.isConnected && (this.isConnected = e, e ? (i = this.reconnected) == null || i.call(this) : (n = this.disconnected) == null || n.call(this)), s && (c(this, e), a(this));
}
setValue(e) {
if (v(this._$Ct)) this._$Ct._$AI(e, this);
else {
const s = [...this._$Ct._$AH];
s[this._$Ci] = e, this._$Ct._$AI(s, this, 0);
}
}
disconnected() {
}
reconnected() {
}
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const P = () => new w();
class w {
}
const d = /* @__PURE__ */ new WeakMap(), U = C(class extends b {
render(t) {
return $;
}
update(t, [e]) {
var i;
const s = e !== this.G;
return s && this.G !== void 0 && this.rt(void 0), (s || this.lt !== this.ct) && (this.G = e, this.ht = (i = t.options) == null ? void 0 : i.host, this.rt(this.ct = t.element)), $;
}
rt(t) {
if (this.isConnected || (t = void 0), typeof this.G == "function") {
const e = this.ht ?? globalThis;
let s = d.get(e);
s === void 0 && (s = /* @__PURE__ */ new WeakMap(), d.set(e, s)), s.get(this.G) !== void 0 && this.G.call(this.ht, void 0), s.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
} else this.G.value = t;
}
get lt() {
var t, e;
return typeof this.G == "function" ? (t = d.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (e = this.G) == null ? void 0 : e.value;
}
disconnected() {
this.lt === this.ct && this.rt(void 0);
}
reconnected() {
this.rt(this.ct);
}
});
export {
O as a,
P as e,
U as n,
T as t
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -160,14 +160,15 @@ table {
display: grid;
min-height: 80px;
grid-template-areas:
". . color-chip"
"title comps color-chip"
"description . color-chip"
"subscription-url . color-chip"
"actions . color-chip"
". . color-chip";
". . 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;
@@ -220,6 +221,8 @@ table {
.actions {
grid-area: actions;
width: fit-content;
display: flex;
gap: 12px;
}
&:hover {

View File

@@ -10,12 +10,4 @@
<pre>{{ addressbook|json }}</pre>
<h2>Delete</h2>
<section>
<form method="POST" action="/frontend/user/{{addressbook.principal}}/addressbook/{{addressbook.id}}/delete">
<button type="submit">Move to trash</button>
</form>
</section>
{% endblock %}

View File

@@ -31,12 +31,4 @@
<pre>{{ calendar|json }}</pre>
<h2>Delete</h2>
<section>
<form method="POST" action="/frontend/user/{{calendar.principal}}/calendar/{{calendar.id}}/delete">
<button type="submit">Move to trash</button>
</form>
</section>
{%endblock %}

View File

@@ -3,6 +3,7 @@
{% block imports %}
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
{% endblock %}
{% block content %}
@@ -67,10 +68,13 @@
<h2>Calendars</h2>
<ul>
{% for calendar in calendars %}
{% let color = calendar.color.to_owned().unwrap_or("red".to_owned()) %}
{% 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">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
<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>
@@ -86,6 +90,9 @@
<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>
@@ -98,10 +105,13 @@
<h3>Deleted Calendars</h3>
<ul>
{% for calendar in deleted_calendars %}
{% let color = calendar.color.to_owned().unwrap_or("red".to_owned()) %}
{% 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">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
<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>
@@ -114,6 +124,7 @@
<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>
@@ -131,7 +142,10 @@
{% for addressbook in addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
<span class="title">{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}</span>
<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>
@@ -139,6 +153,7 @@
<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>
@@ -152,7 +167,10 @@
{% for addressbook in deleted_addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
<span class="title">{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}</span>
<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>
@@ -160,6 +178,7 @@
<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>

View File

@@ -12,3 +12,12 @@ pub struct FrontendConfig {
#[serde(default = "default_true")]
pub allow_password_login: bool,
}
impl Default for FrontendConfig {
fn default() -> Self {
Self {
enabled: true,
allow_password_login: true,
}
}
}

View File

@@ -26,9 +26,9 @@ pub use config::FrontendConfig;
use oidc_user_store::OidcUserStore;
use crate::routes::{
addressbook::{route_addressbook, route_addressbook_restore, route_delete_addressbook},
addressbook::{route_addressbook, route_addressbook_restore},
app_token::{route_delete_app_token, route_post_app_token},
calendar::{route_calendar, route_calendar_restore, route_delete_calendar},
calendar::{route_calendar, route_calendar_restore},
login::{route_get_login, route_post_login, route_post_logout},
user::{route_get_home, route_root, route_user_named},
};
@@ -60,10 +60,6 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
"/user/{user}/calendar/{calendar}",
get(route_calendar::<CS>),
)
.route(
"/user/{user}/calendar/{calendar}/delete",
post(route_delete_calendar::<CS>),
)
.route(
"/user/{user}/calendar/{calendar}/restore",
post(route_calendar_restore::<CS>),
@@ -73,10 +69,6 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
"/user/{user}/addressbook/{addressbook}",
get(route_addressbook::<AS>),
)
.route(
"/user/{user}/addressbook/{addressbook}/delete",
post(route_delete_addressbook::<AS>),
)
.route(
"/user/{user}/addressbook/{addressbook}/restore",
post(route_addressbook_restore::<AS>),

View File

@@ -47,19 +47,3 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
None => (StatusCode::CREATED, "Restored").into_response(),
})
}
pub async fn route_delete_addressbook<AS: AddressbookStore>(
Path((owner, addressbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>,
user: Principal,
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
store
.delete_addressbook(&owner, &addressbook_id, true)
.await?;
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
}

View File

@@ -27,7 +27,7 @@ pub async fn route_calendar<C: CalendarStore>(
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
Ok(CalendarPage {
calendar: store.get_calendar(&owner, &cal_id).await?,
calendar: store.get_calendar(&owner, &cal_id, true).await?,
}
.into_response())
}
@@ -47,17 +47,3 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
None => (StatusCode::CREATED, "Restored").into_response(),
})
}
pub async fn route_delete_calendar<C: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<C>>,
user: Principal,
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
store.delete_calendar(&owner, &cal_id, true).await?;
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
}

View File

@@ -62,14 +62,14 @@ impl AddressObject {
&self.vcf
}
pub fn get_anniversary(&self) -> Option<CalDateTime> {
let prop = self.vcard.get_property("ANNIVERSARY")?;
CalDateTime::parse_prop(prop, &HashMap::default()).ok()
pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?;
CalDateTime::parse_vcard(prop).ok()
}
pub fn get_birthday(&self) -> Option<CalDateTime> {
let prop = self.vcard.get_property("BDAY")?;
CalDateTime::parse_prop(prop, &HashMap::default()).ok()
pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("BDAY")?.value.as_deref()?;
CalDateTime::parse_vcard(prop).ok()
}
pub fn get_full_name(&self) -> Option<&str> {
@@ -78,25 +78,27 @@ impl AddressObject {
}
pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok(if let Some(anniversary) = self.get_anniversary() {
let fullname = if let Some(name) = self.get_full_name() {
name
} else {
return Ok(None);
};
let anniversary = anniversary.date();
let year = anniversary.year();
let anniversary_start = anniversary.format(LOCAL_DATE);
let anniversary_end = anniversary
.succ_opt()
.unwrap_or(anniversary)
.format(LOCAL_DATE);
let uid = format!("{}-anniversary", self.get_id());
Ok(
if let Some((anniversary, contains_year)) = self.get_anniversary() {
let fullname = if let Some(name) = self.get_full_name() {
name
} else {
return Ok(None);
};
let anniversary = anniversary.date();
let year = contains_year.then_some(anniversary.year());
let anniversary_start = anniversary.format(LOCAL_DATE);
let anniversary_end = anniversary
.succ_opt()
.unwrap_or(anniversary)
.format(LOCAL_DATE);
let uid = format!("{}-anniversary", self.get_id());
Some(CalendarObject::from_ics(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -105,39 +107,42 @@ DTSTART;VALUE=DATE:{anniversary_start}
DTEND;VALUE=DATE:{anniversary_end}
UID:{uid}
RRULE:FREQ=YEARLY
SUMMARY:💍 {fullname} ({year})
SUMMARY:💍 {fullname}{year_suffix}
TRANSP:TRANSPARENT
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY
DESCRIPTION:💍 {fullname} ({year})
DESCRIPTION:💍 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR"#,
),
)?)
} else {
None
})
),
)?)
} else {
None
},
)
}
pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok(if let Some(birthday) = self.get_birthday() {
let fullname = if let Some(name) = self.get_full_name() {
name
} else {
return Ok(None);
};
let birthday = birthday.date();
let year = birthday.year();
let birthday_start = birthday.format(LOCAL_DATE);
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
let uid = format!("{}-birthday", self.get_id());
Ok(
if let Some((birthday, contains_year)) = self.get_birthday() {
let fullname = if let Some(name) = self.get_full_name() {
name
} else {
return Ok(None);
};
let birthday = birthday.date();
let year = contains_year.then_some(birthday.year());
let birthday_start = birthday.format(LOCAL_DATE);
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
let uid = format!("{}-birthday", self.get_id());
Some(CalendarObject::from_ics(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -146,20 +151,21 @@ DTSTART;VALUE=DATE:{birthday_start}
DTEND;VALUE=DATE:{birthday_end}
UID:{uid}
RRULE:FREQ=YEARLY
SUMMARY:🎂 {fullname} ({year})
SUMMARY:🎂 {fullname}{year_suffix}
TRANSP:TRANSPARENT
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY
DESCRIPTION:🎂 {fullname} ({year})
DESCRIPTION:🎂 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR"#,
),
)?)
} else {
None
})
),
)?)
} else {
None
},
)
}
/// Get significant dates associated with this address object

View File

@@ -254,6 +254,16 @@ impl CalDateTime {
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
return Ok(CalDateTime::Date(date, timezone));
}
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
}
// Also returns whether the date contains a year
pub fn parse_vcard(value: &str) -> Result<(Self, bool), CalDateTimeError> {
if let Ok(datetime) = Self::parse(value, None) {
return Ok((datetime, true));
}
if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) {
// Because 1972 is a leap year
let year = 1972;
@@ -261,13 +271,15 @@ impl CalDateTime {
let month = captures.name("m").unwrap().as_str().parse().ok().unwrap();
let day = captures.name("d").unwrap().as_str().parse().ok().unwrap();
return Ok(CalDateTime::Date(
NaiveDate::from_ymd_opt(year, month, day)
.ok_or(CalDateTimeError::ParseError(value.to_string()))?,
timezone,
return Ok((
CalDateTime::Date(
NaiveDate::from_ymd_opt(year, month, day)
.ok_or(CalDateTimeError::ParseError(value.to_string()))?,
CalTimezone::Local,
),
false,
));
}
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
}
@@ -407,24 +419,33 @@ mod tests {
#[test]
fn test_vcard_date() {
assert_eq!(
CalDateTime::parse("19850412", None).unwrap(),
CalDateTime::Date(
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local
CalDateTime::parse_vcard("19850412").unwrap(),
(
CalDateTime::Date(
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local
),
true
)
);
assert_eq!(
CalDateTime::parse("1985-04-12", None).unwrap(),
CalDateTime::Date(
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local
CalDateTime::parse_vcard("1985-04-12").unwrap(),
(
CalDateTime::Date(
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local
),
true
)
);
assert_eq!(
CalDateTime::parse("--0412", None).unwrap(),
CalDateTime::Date(
NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(),
crate::CalTimezone::Local
CalDateTime::parse_vcard("--0412").unwrap(),
(
CalDateTime::Date(
NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(),
crate::CalTimezone::Local
),
false
)
);
}

View File

@@ -7,6 +7,9 @@ pub enum OidcError {
#[error("Cannot generate redirect url, something's not configured correctly")]
OidcParseError(#[from] ParseError),
#[error("Error fetching user info: {0}")]
UserInfo(String),
#[error(transparent)]
OidcConfigurationError(#[from] ConfigurationError),

View File

@@ -41,7 +41,7 @@ struct OidcState {
#[derive(Debug, Deserialize, Serialize)]
struct GroupAdditionalClaims {
#[serde(default)]
pub groups: Vec<String>,
groups: Option<Vec<String>>,
}
impl openidconnect::AdditionalClaims for GroupAdditionalClaims {}
@@ -190,12 +190,14 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
)?
.request_async(&http_client)
.await
.map_err(|_| OidcError::Other("Error fetching user info"))?;
.map_err(|e| OidcError::UserInfo(e.to_string()))?;
if let Some(require_group) = &oidc_config.require_group {
if !user_info_claims
.additional_claims()
.groups
.clone()
.unwrap_or_default()
.contains(require_group)
{
return Ok((

View File

@@ -3,7 +3,10 @@ mod principal;
use crate::error::Error;
use async_trait::async_trait;
pub use principal::{AppToken, Principal, PrincipalType};
mod principal_type;
pub use principal_type::*;
pub use principal::{AppToken, Principal};
#[async_trait]
pub trait AuthenticationProvider: Send + Sync + 'static {

View File

@@ -1,3 +1,4 @@
use crate::{Secret, auth::PrincipalType};
use axum::{
body::Body,
extract::{FromRequestParts, OptionalFromRequestParts},
@@ -6,67 +7,8 @@ use axum::{
use chrono::{DateTime, Utc};
use derive_more::Display;
use http::{HeaderValue, StatusCode, header};
use rustical_xml::ValueSerialize;
use serde::{Deserialize, Serialize};
use std::{convert::Infallible, fmt::Display};
use crate::Secret;
/// https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.3
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PrincipalType {
#[default]
Individual,
Group,
Resource,
Room,
Unknown,
// TODO: X-Name, IANA-token
}
impl TryFrom<&str> for PrincipalType {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"INDIVIDUAL" => Self::Individual,
"GROUP" => Self::Group,
"RESOURCE" => Self::Resource,
"ROOM" => Self::Room,
"UNKNOWN" => Self::Unknown,
_ => {
return Err(crate::Error::InvalidPrincipalType(
"Invalid principal type".to_owned(),
));
}
})
}
}
impl PrincipalType {
pub fn as_str(&self) -> &'static str {
match self {
PrincipalType::Individual => "INDIVIDUAL",
PrincipalType::Group => "GROUP",
PrincipalType::Resource => "RESOURCE",
PrincipalType::Room => "ROOM",
PrincipalType::Unknown => "UNKNOWN",
}
}
}
impl Display for PrincipalType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl ValueSerialize for PrincipalType {
fn serialize(&self) -> String {
self.to_string()
}
}
use std::convert::Infallible;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AppToken {

View File

@@ -0,0 +1,60 @@
use std::fmt::Display;
use rustical_xml::ValueSerialize;
use serde::{Deserialize, Serialize};
/// https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.3
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PrincipalType {
#[default]
Individual,
Group,
Resource,
Room,
Unknown,
// TODO: X-Name, IANA-token
}
impl TryFrom<&str> for PrincipalType {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"INDIVIDUAL" => Self::Individual,
"GROUP" => Self::Group,
"RESOURCE" => Self::Resource,
"ROOM" => Self::Room,
"UNKNOWN" => Self::Unknown,
_ => {
return Err(crate::Error::InvalidPrincipalType(
"Invalid principal type".to_owned(),
));
}
})
}
}
impl PrincipalType {
pub fn as_str(&self) -> &'static str {
match self {
PrincipalType::Individual => "INDIVIDUAL",
PrincipalType::Group => "GROUP",
PrincipalType::Resource => "RESOURCE",
PrincipalType::Room => "ROOM",
PrincipalType::Unknown => "UNKNOWN",
}
}
}
impl Display for PrincipalType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl ValueSerialize for PrincipalType {
fn serialize(&self) -> String {
self.to_string()
}
}

View File

@@ -11,7 +11,12 @@ pub struct CalendarQuery {
#[async_trait]
pub trait CalendarStore: Send + Sync + 'static {
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error>;
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error>;
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;

View File

@@ -1,8 +1,7 @@
use std::sync::Arc;
use async_trait::async_trait;
use derive_more::Constructor;
use rustical_ical::CalendarObject;
use std::sync::Arc;
use crate::{
Calendar, CalendarStore, Error, calendar_store::CalendarQuery,
@@ -27,11 +26,20 @@ impl<CS: CalendarStore, BS: CalendarStore> Clone for CombinedCalendarStore<CS, B
#[async_trait]
impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarStore<CS, BS> {
#[inline]
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
if id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store.get_calendar(principal, id).await
self.birthday_store
.get_calendar(principal, id, show_deleted)
.await
} else {
self.cal_store.get_calendar(principal, id).await
self.cal_store
.get_calendar(principal, id, show_deleted)
.await
}
}

View File

@@ -38,11 +38,17 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
/// Objects are all prefixed with BIRTHDAYS_PREFIX
#[async_trait]
impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
let addressbook = self.0.get_addressbook(principal, id, false).await?;
let addressbook = self.0.get_addressbook(principal, id, show_deleted).await?;
Ok(birthday_calendar(addressbook))
}
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let addressbooks = self.0.get_addressbooks(principal).await?;
Ok(addressbooks.into_iter().map(birthday_calendar).collect())

View File

@@ -11,6 +11,9 @@ mod secret;
mod subscription_store;
pub mod synctoken;
#[cfg(test)]
pub mod tests;
pub use addressbook_store::AddressbookStore;
pub use calendar_store::CalendarStore;
pub use combined_calendar_store::CombinedCalendarStore;

View File

View File

@@ -1,35 +0,0 @@
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;UNTIL=20370329T010000Z;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;UNTIL=20361026T010000Z;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:67d830c3e681950b6a12f7c287b316269a19fcf7
DTSTAMP:20230831T102923Z
DTSTART;TZID=Europe/Berlin:20230829T043000
DTEND;TZID=Europe/Berlin:20230829T045500
SEQUENCE:2
SUMMARY:asdjlk
TRANSP:OPAQUE
CLASS:PUBLIC
CREATED:20230831T103040Z
LAST-MODIFIED:20230831T103040Z
END:VEVENT
END:VCALENDAR

View File

@@ -1,109 +0,0 @@
BEGIN:VCALENDAR
BEGIN:VTIMEZONE
TZID:Europe/Berlin
LAST-MODIFIED:20230104T023643Z
TZURL:https://www.tzurl.org/zoneinfo/Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
X-PROLEPTIC-TZNAME:LMT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+005328
TZOFFSETTO:+0100
DTSTART:18930401T000632
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19160430T230000
RDATE:19400401T020000
RDATE:19430329T020000
RDATE:19460414T020000
RDATE:19470406T030000
RDATE:19480418T020000
RDATE:19490410T020000
RDATE:19800406T020000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19161001T010000
RDATE:19421102T030000
RDATE:19431004T030000
RDATE:19441002T030000
RDATE:19451118T030000
RDATE:19461007T030000
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19170416T020000
RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19170917T030000
RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19440403T020000
RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEMT
TZOFFSETFROM:+0200
TZOFFSETTO:+0300
DTSTART:19450524T010000
RDATE:19470511T020000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0300
TZOFFSETTO:+0200
DTSTART:19450924T030000
RDATE:19470629T030000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0100
TZOFFSETTO:+0100
DTSTART:19460101T000000
RDATE:19800101T000000
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19471005T030000
RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19800928T030000
RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
END:VCALENDAR

View File

@@ -1,46 +0,0 @@
// use rstest::rstest;
// use rstest_reuse::{self, apply, template};
// use rustical_store::{CalendarObject, CalendarStore};
// use rustical_store_sqlite::{calendar_store::SqliteCalendarStore, create_test_db};
//
// const TIMEZONE: &str = include_str!("examples/timezone.ics");
// const EVENT: &str = include_str!("examples/event.ics");
//
// #[template]
// #[rstest]
// #[case::sqlite(async {
// let (send, _recv) = tokio::sync::mpsc::channel(100);
// SqliteCalendarStore::new(create_test_db().await.unwrap(), send)
// })]
// async fn cal_store<CS: CalendarStore>(
// #[future(awt)]
// #[case]
// mut store: CS,
// ) {
// }
// TODO: Reimplement, add test principal
// #[apply(cal_store)]
// #[tokio::test]
// async fn test_create_event<CS: CalendarStore>(store: CS) {
// store
// .insert_calendar(rustical_store::Calendar {
// id: "test".to_owned(),
// displayname: Some("Test Calendar".to_owned()),
// principal: "testuser".to_owned(),
// timezone: Some(TIMEZONE.to_owned()),
// ..Default::default() // timezone: TIMEZONE.to_owned(),
// })
// .await
// .unwrap();
//
// let object = CalendarObject::from_ics("asd".to_owned(), EVENT.to_owned()).unwrap();
// store
// .put_object("testuser".to_owned(), "test".to_owned(), object, true)
// .await
// .unwrap();
//
// let event = store.get_object("testuser", "test", "asd").await.unwrap();
// assert_eq!(event.get_ics(), EVENT);
// assert_eq!(event.get_id(), "asd");
// }

View File

@@ -7,6 +7,9 @@ repository.workspace = true
license.workspace = true
publish = false
[features]
test = []
[dependencies]
tokio.workspace = true
rustical_store = { workspace = true }

View File

@@ -86,14 +86,17 @@ impl SqliteCalendarStore {
executor: E,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let cal = sqlx::query_as!(
CalendarRow,
r#"SELECT *
FROM calendars
WHERE (principal, id) = (?, ?)"#,
WHERE (principal, id) = (?, ?)
AND ((deleted_at IS NULL) OR ?) "#,
principal,
id
id,
show_deleted
)
.fetch_one(executor)
.await
@@ -470,8 +473,13 @@ impl SqliteCalendarStore {
#[async_trait]
impl CalendarStore for SqliteCalendarStore {
#[instrument]
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
Self::_get_calendar(&self.db, principal, id).await
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
Self::_get_calendar(&self.db, principal, id, show_deleted).await
}
#[instrument]
@@ -509,7 +517,7 @@ impl CalendarStore for SqliteCalendarStore {
) -> Result<(), Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let cal = match Self::_get_calendar(&mut *tx, principal, id).await {
let cal = match Self::_get_calendar(&mut *tx, principal, id, true).await {
Ok(cal) => Some(cal),
Err(Error::NotFound) => None,
Err(err) => return Err(err),
@@ -599,7 +607,10 @@ impl CalendarStore for SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(&principal, &cal_id).await?.push_topic,
topic: self
.get_calendar(&principal, &cal_id, true)
.await?
.push_topic,
}) {
error!("Push notification about deleted calendar failed: {err}");
};
@@ -624,7 +635,7 @@ impl CalendarStore for SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(principal, cal_id).await?.push_topic,
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
}) {
error!("Push notification about deleted calendar failed: {err}");
};
@@ -649,7 +660,7 @@ impl CalendarStore for SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(principal, cal_id).await?.push_topic,
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
}) {
error!("Push notification about deleted calendar failed: {err}");
};

View File

@@ -8,6 +8,9 @@ pub mod error;
pub mod principal_store;
pub mod subscription_store;
#[cfg(any(test, feature = "test"))]
pub mod tests;
#[derive(Debug, Clone, Serialize, sqlx::Type)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum ChangeOperation {
@@ -42,13 +45,3 @@ pub async fn create_db_pool(db_url: &str, migrate: bool) -> Result<Pool<Sqlite>,
}
Ok(db)
}
pub async fn create_test_db() -> Result<SqlitePool, sqlx::Error> {
let db = SqlitePool::connect("sqlite::memory:").await?;
sqlx::migrate!("./migrations").run(&db).await?;
Ok(db)
}
pub async fn create_test_store() -> Result<SqliteStore, sqlx::Error> {
Ok(SqliteStore::new(create_test_db().await?))
}

View File

@@ -0,0 +1,42 @@
use crate::{
SqliteStore, addressbook_store::SqliteAddressbookStore, calendar_store::SqliteCalendarStore,
principal_store::SqlitePrincipalStore,
};
use rustical_store::{
AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore,
auth::AuthenticationProvider,
};
use sqlx::SqlitePool;
use std::sync::Arc;
use tokio::sync::mpsc::Receiver;
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);
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,
)
}
#[tokio::test]
async fn test_create_store() {
get_test_stores().await;
}

View File

@@ -0,0 +1,7 @@
# Frontend
The frontend is currently generated through [askama templates](https://askama.readthedocs.io/en/stable/) for server-side rendered pages
and uses Web Components for interactive elements.
Normally, content that will be statically served by the frontend module (i.e. stylesheet and web components) is embedded into the binary.
Using the `frontend-dev` feature you can serve it from source to see changes without recompiling RustiCal.

View File

@@ -3,11 +3,11 @@
Collection of RFCs relevant to this project
- Versioning Extensions to WebDAV: [RFC 3253](https://datatracker.ietf.org/doc/html/rfc3253)
- provides the REPORT method
- provides the REPORT method
- Calendaring Extensions to WebDAV (CalDAV): [RFC 4791](https://datatracker.ietf.org/doc/html/rfc4791)
- Scheduling Extensions to CalDAV: [RFC 6638](https://datatracker.ietf.org/doc/html/rfc6638)
- not sur`e yet whether to implement this
- not sure yet whether to implement this
- Collection Synchronization WebDAV [RFC 6578](https://datatracker.ietf.org/doc/html/rfc6578)
- We need to implement sync-token, etc.
- This is important for more efficient synchronisation
- We need to implement sync-token, etc.
- This is important for more efficient synchronisation
- iCalendar [RFC 2445](https://datatracker.ietf.org/doc/html/rfc2445#section-3.10)

View File

@@ -3,10 +3,10 @@
a CalDAV/CardDAV server
!!! warning
RustiCal is **not production-ready!**
I've been using it for the last few weeks and I'm slowly becoming more confident,
however you'd be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
RustiCal is **not production-ready!**
I've been using it for the last few weeks and I'm slowly becoming more confident,
however you'd be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
## Features

View File

@@ -0,0 +1,31 @@
# Configuration
While RustiCal (apart from user management) will work without any configuration you should still know how to configure it. :)
You can either mount a `config.toml` file or use environment variables (recommended).
To see the options you can generate a default configuration using
```sh title="Generate default config.toml"
rustical gen-config
```
To see all configuration options available you can browse the [Cargo docs](/rustical/_crate/rustical/config/struct.Config.html).
## Environment variables
The options in `config.toml` can also be configured using environment variables.
Names translate the following:
```toml title="Example config.toml"
[data_store.toml]
path = "asd"
```
becomes `RUSTICAL_DATA_STORE__TOML__PATH`.
Every variable is
- uppercase
- prefixed by `RUSTICAL_`
- Dots become `__`
- Arrays are JSON-encoded

View File

@@ -40,38 +40,6 @@ App tokens are used by your CalDAV/CardDAV client (which can be managed through
I recommend to generate random app tokens for each CalDAV/CardDAV client.
Since the app tokens are random they use the faster `pbkdf2` algorithm.
## Configuration
While RustiCal (apart from user management) will work without any configuration you should still know how to configure it. :)
You can either mount a `config.toml` file or use environment variables.
To see the options you can generate a default configuration using
```sh title="Generate default config.toml"
rustical gen-config
```
To see all configuration options available you can browse the [Cargo docs](/rustical/_crate/rustical/config/struct.Config.html).
### Environment variables
The options in `config.toml` can also be configured using environment variables.
Names translate the following:
```toml title="Example config.toml"
[data_store.toml]
path = "asd"
```
becomes `RUSTICAL_DATA_STORE__TOML__PATH`.
Every variable is
- uppercase
- prefixed by `RUSTICAL_`
- Dots become `__`
- Arrays are JSON-encoded
## Manual
```sh

10
docs/style.css Normal file
View File

@@ -0,0 +1,10 @@
body .md-main {
h1 {
font-weight: bold;
color: var(--md-typeset-color);
}
}
.md-tabs {
box-shadow: 0px 0 20px -10px black;
}

View File

@@ -14,7 +14,7 @@ theme:
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
primary: white
accent: indigo
toggle:
icon: material/toggle-switch
@@ -38,6 +38,13 @@ theme:
- content.tooltips
- navigation.indices
- navigation.tabs
- navigation.indexes
- navigation.indexes
- navigation.instant
- navigation.footer
extra_css:
- style.css
markdown_extensions:
- fenced_code
@@ -58,10 +65,13 @@ markdown_extensions:
nav:
- Home: index.md
- Installation: installation.md
- Installation:
- installation/index.md
- Configuration: installation/configuration.md
- OpenID Connect: setup/oidc.md
- Developers:
- developers/index.md
- Relevant RFCs: developers/rfcs.md
- Frontend: developers/frontend.md
- Debugging: developers/debugging.md
- Cargo docs: /rustical/_crate/rustical/

View File

@@ -79,6 +79,7 @@ pub struct Config {
pub data_store: DataStoreConfig,
#[serde(default)]
pub http: HttpConfig,
#[serde(default)]
pub frontend: FrontendConfig,
#[serde(default)]
pub oidc: Option<OidcConfig>,