Compare commits

..

30 Commits

Author SHA1 Message Date
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
Lennart
7d18faff69 version 0.3.6 2025-06-23 11:21:04 +02:00
Lennart
753f8e90d3 fix(frontend): Fix calendar download link 2025-06-23 11:20:44 +02:00
Lennart
701fa9dd9c Version 3.4.5 2025-06-23 08:54:26 +02:00
Lennart
31b17cfe7f Frontend: Fix dumb typo in calendar creation form
Fixes #82
2025-06-23 08:53:50 +02:00
Lennart
d802a0085a Add Home Assistant to tested clients 2025-06-23 00:42:45 +02:00
Lennart
786b15f5b9 version 0.3.4 2025-06-22 23:58:49 +02:00
Lennart
f5d097ac55 oidc: Fix for OIDC servers not supporting RFC 9207
see #81
2025-06-22 23:55:57 +02:00
Lennart
668fa86e3c Update version to 0.3.3 2025-06-22 21:46:37 +02:00
Lennart
23d2024644 Update note on production-readiness 2025-06-22 19:43:46 +02:00
Lennart
15aadcf1be Rename User struct to Principal 2025-06-19 20:59:59 +02:00
Lennart
4a3b7d7ce6 Update typescript config 2025-06-19 20:52:17 +02:00
Lennart
1a2f3b8f8a frontend: Move collection creation to dialog 2025-06-18 18:09:19 +02:00
Lennart
9e8c218308 Remove unused p256 dependency 2025-06-18 17:49:00 +02:00
Lennart
f2adce739b Update version to v0.3.2 2025-06-15 17:12:34 +02:00
82 changed files with 4585 additions and 4146 deletions

View File

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

View File

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

268
Cargo.lock generated
View File

@@ -13,9 +13,9 @@ dependencies = [
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
@@ -49,9 +49,9 @@ dependencies = [
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anstyle-parse", "anstyle-parse",
@@ -64,33 +64,33 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.10" version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [ dependencies = [
"utf8parse", "utf8parse",
] ]
[[package]] [[package]]
name = "anstyle-query" name = "anstyle-query"
version = "1.1.2" version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
name = "anstyle-wincon" name = "anstyle-wincon"
version = "3.0.8" version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell_polyfill", "once_cell_polyfill",
@@ -162,9 +162,9 @@ dependencies = [
[[package]] [[package]]
name = "askama_web" name = "askama_web"
version = "0.14.3" version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a91fdeb04bf77d96234780cdd58fc221eb10de7031e1782a22f40fc8ac1a313" checksum = "83731f1a2286209c2b679445e8faaa53270646a90c509bf92729e966d198cb6b"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web_derive", "askama_web_derive",
@@ -206,9 +206,9 @@ dependencies = [
[[package]] [[package]]
name = "atomic" name = "atomic"
version = "0.6.0" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
] ]
@@ -221,9 +221,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "axum" name = "axum"
@@ -379,15 +379,15 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.17.0" version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.23.0" version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@@ -403,18 +403,18 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.25" version = "1.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]] [[package]]
name = "cfg_aliases" name = "cfg_aliases"
@@ -469,9 +469,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.39" version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -479,9 +479,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.39" version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -491,9 +491,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.32" version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -503,15 +503,15 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.7.4" version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
@@ -1068,7 +1068,7 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.1+wasi-snapshot-preview1",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -1136,9 +1136,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.3" version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [ dependencies = [
"allocator-api2", "allocator-api2",
"equivalent", "equivalent",
@@ -1151,7 +1151,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [ dependencies = [
"hashbrown 0.15.3", "hashbrown 0.15.4",
] ]
[[package]] [[package]]
@@ -1295,9 +1295,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.27.6" version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [ dependencies = [
"http", "http",
"hyper", "hyper",
@@ -1325,9 +1325,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.13" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -1512,7 +1512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.3", "hashbrown 0.15.4",
"serde", "serde",
] ]
@@ -1589,9 +1589,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]] [[package]]
name = "libm" name = "libm"
@@ -1678,9 +1678,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]] [[package]]
name = "mime" name = "mime"
@@ -1700,9 +1700,9 @@ dependencies = [
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.8" version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [ dependencies = [
"adler2", "adler2",
] ]
@@ -1714,7 +1714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [ dependencies = [
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -2370,9 +2370,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-udp" name = "quinn-udp"
version = "0.5.12" version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
dependencies = [ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
@@ -2393,9 +2393,9 @@ dependencies = [
[[package]] [[package]]
name = "r-efi" name = "r-efi"
version = "5.2.0" version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "rand" name = "rand"
@@ -2458,13 +2458,33 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.12" version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
dependencies = [ dependencies = [
"bitflags", "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]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@@ -2517,9 +2537,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.19" version = "0.12.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -2534,11 +2554,9 @@ dependencies = [
"hyper", "hyper",
"hyper-rustls", "hyper-rustls",
"hyper-util", "hyper-util",
"ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
@@ -2715,9 +2733,9 @@ dependencies = [
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
@@ -2736,7 +2754,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -2779,7 +2797,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2814,7 +2832,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2846,7 +2864,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2871,7 +2889,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2883,7 +2901,6 @@ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"log", "log",
"openssl", "openssl",
"p256",
"quick-xml", "quick-xml",
"rand 0.9.1", "rand 0.9.1",
"reqwest", "reqwest",
@@ -2898,7 +2915,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -2931,7 +2948,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -2949,7 +2966,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2964,7 +2981,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -2998,7 +3015,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3018,7 +3035,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.3.0" version = "0.4.4"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.12", "thiserror 2.0.12",
@@ -3027,9 +3044,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.27" version = "0.23.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"ring", "ring",
@@ -3081,6 +3098,18 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -3170,9 +3199,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.8" version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -3191,15 +3220,16 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.12.0" version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.9.0", "indexmap 2.9.0",
"schemars",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
@@ -3209,9 +3239,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_with_macros" name = "serde_with_macros"
version = "3.12.0" version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
@@ -3283,18 +3313,15 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.0" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -3358,7 +3385,7 @@ dependencies = [
"futures-intrusive", "futures-intrusive",
"futures-io", "futures-io",
"futures-util", "futures-util",
"hashbrown 0.15.3", "hashbrown 0.15.4",
"hashlink", "hashlink",
"indexmap 2.9.0", "indexmap 2.9.0",
"log", "log",
@@ -3574,9 +3601,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.101" version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3645,12 +3672,11 @@ dependencies = [
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.8" version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell",
] ]
[[package]] [[package]]
@@ -3775,9 +3801,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.22" version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@@ -3787,18 +3813,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.9" version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.26" version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [ dependencies = [
"indexmap 2.9.0", "indexmap 2.9.0",
"serde", "serde",
@@ -3810,9 +3836,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_write" name = "toml_write"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "tonic" name = "tonic"
@@ -3980,9 +4006,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.28" version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3991,9 +4017,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.33" version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable", "valuable",
@@ -4187,9 +4213,9 @@ dependencies = [
[[package]] [[package]]
name = "wasi" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "wasi" name = "wasi"
@@ -4299,9 +4325,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502"
dependencies = [ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
@@ -4384,9 +4410,9 @@ dependencies = [
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.1.1" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]] [[package]]
name = "windows-result" name = "windows-result"
@@ -4556,9 +4582,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.10" version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -4621,18 +4647,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.25" version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.25" version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.3.0" version = "0.4.4"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical" repository = "https://github.com/lennart-k/rustical"
@@ -139,7 +139,6 @@ ece = { version = "2.3", default-features = false, features = [
"backend-openssl", "backend-openssl",
] } ] }
openssl = { version = "0.10", features = ["vendored"] } openssl = { version = "0.10", features = ["vendored"] }
p256 = { version = "0.13", features = ["ecdh"] }
[dependencies] [dependencies]
rustical_store = { workspace = true } rustical_store = { workspace = true }

View File

@@ -4,8 +4,8 @@ a CalDAV/CardDAV server
> [!WARNING] > [!WARNING]
RustiCal is **not production-ready!** RustiCal is **not production-ready!**
While I've started migrating to RustiCal and becoming more confident, I've been using RustiCal for the last few weeks and I'm slowly becoming more confident,
please know that bugs and rough edges will still occur. 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. :) 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 ## Features
@@ -30,3 +30,4 @@ a CalDAV/CardDAV server
- GNOME Accounts, GNOME Calendar, GNOME Contacts - GNOME Accounts, GNOME Calendar, GNOME Contacts
- Evolution - Evolution
- Apple Calendar - Apple Calendar
- Home Assistant integration

View File

@@ -9,7 +9,7 @@ use ical::generator::{Emitter, IcalCalendarBuilder};
use ical::property::Property; use ical::property::Property;
use percent_encoding::{CONTROLS, utf8_percent_encode}; use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_ical::{CalendarObjectComponent, EventObject, JournalObject, TodoObject}; use rustical_ical::{CalendarObjectComponent, EventObject, JournalObject, TodoObject};
use rustical_store::{CalendarStore, SubscriptionStore, auth::User}; use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
@@ -18,18 +18,22 @@ use tracing::instrument;
pub async fn route_get<C: CalendarStore, S: SubscriptionStore>( pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
Path((principal, calendar_id)): Path<(String, String)>, Path((principal, calendar_id)): Path<(String, String)>,
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>, State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
user: User, user: Principal,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?; let calendar = cal_store
.get_calendar(&principal, &calendar_id, true)
.await?;
if !user.is_principal(&calendar.principal) { if !user.is_principal(&calendar.principal) {
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?; let calendar = cal_store
.get_calendar(&principal, &calendar_id, true)
.await?;
let mut timezones = HashMap::new(); let mut timezones = HashMap::new();
let objects = cal_store.get_objects(&principal, &calendar_id).await?; let objects = cal_store.get_objects(&principal, &calendar_id).await?;

View File

@@ -6,7 +6,7 @@ use axum::response::{IntoResponse, Response};
use http::{Method, StatusCode}; use http::{Method, StatusCode};
use rustical_dav::xml::HrefElement; use rustical_dav::xml::HrefElement;
use rustical_ical::CalendarObjectType; use rustical_ical::CalendarObjectType;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{Calendar, CalendarStore, SubscriptionStore}; use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument; use tracing::instrument;
@@ -63,7 +63,7 @@ struct MkcolRequest {
#[instrument(skip(cal_store))] #[instrument(skip(cal_store))]
pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>( pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>, Path((principal, cal_id)): Path<(String, String)>,
user: User, user: Principal,
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>, State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
method: Method, method: Method,
body: String, body: String,

View File

@@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderValue, StatusCode, header};
use rustical_dav::privileges::UserPrivilege; use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_dav_push::register::PushRegister; use rustical_dav_push::register::PushRegister;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{CalendarStore, Subscription, SubscriptionStore}; use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
use rustical_xml::XmlDocument; use rustical_xml::XmlDocument;
use tracing::instrument; use tracing::instrument;
@@ -15,7 +15,7 @@ use tracing::instrument;
#[instrument(skip(resource_service))] #[instrument(skip(resource_service))]
pub async fn route_post<C: CalendarStore, S: SubscriptionStore>( pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>, Path((principal, cal_id)): Path<(String, String)>,
user: User, user: Principal,
State(resource_service): State<CalendarResourceService<C, S>>, State(resource_service): State<CalendarResourceService<C, S>>,
body: String, body: String,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
@@ -25,7 +25,7 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
let calendar = resource_service let calendar = resource_service
.cal_store .cal_store
.get_calendar(&principal, &cal_id) .get_calendar(&principal, &cal_id, false)
.await?; .await?;
let calendar_resource = CalendarResource { let calendar_resource = CalendarResource {
cal: calendar, cal: calendar,

View File

@@ -21,7 +21,7 @@ use rustical_dav::{
}, },
}; };
use rustical_ical::CalendarObject; use rustical_ical::CalendarObject;
use rustical_store::{CalendarStore, SubscriptionStore, auth::User}; use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
use rustical_xml::{XmlDeserialize, XmlDocument}; use rustical_xml::{XmlDeserialize, XmlDocument};
use sync_collection::handle_sync_collection; use sync_collection::handle_sync_collection;
use tracing::instrument; use tracing::instrument;
@@ -56,7 +56,7 @@ fn objects_response(
path: &str, path: &str,
principal: &str, principal: &str,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &PropfindType<CalendarObjectPropWrapperName>, prop: &PropfindType<CalendarObjectPropWrapperName>,
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> { ) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
let mut responses = Vec::new(); let mut responses = Vec::new();
@@ -90,7 +90,7 @@ fn objects_response(
#[instrument(skip(cal_store))] #[instrument(skip(cal_store))]
pub async fn route_report_calendar<C: CalendarStore, S: SubscriptionStore>( pub async fn route_report_calendar<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>, Path((principal, cal_id)): Path<(String, String)>,
user: User, user: Principal,
Extension(puri): Extension<CalDavPrincipalUri>, Extension(puri): Extension<CalDavPrincipalUri>,
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>, State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
OriginalUri(uri): OriginalUri, OriginalUri(uri): OriginalUri,

View File

@@ -13,7 +13,7 @@ use rustical_dav::{
}; };
use rustical_store::{ use rustical_store::{
CalendarStore, CalendarStore,
auth::User, auth::Principal,
synctoken::{format_synctoken, parse_synctoken}, synctoken::{format_synctoken, parse_synctoken},
}; };
@@ -21,7 +21,7 @@ pub async fn handle_sync_collection<C: CalendarStore>(
sync_collection: &SyncCollectionRequest<CalendarObjectPropWrapperName>, sync_collection: &SyncCollectionRequest<CalendarObjectPropWrapperName>,
path: &str, path: &str,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
cal_store: &C, cal_store: &C,

View File

@@ -12,7 +12,7 @@ use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedR
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp}; use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
use rustical_ical::CalDateTime; use rustical_ical::CalDateTime;
use rustical_store::Calendar; use rustical_store::Calendar;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_xml::{EnumVariants, PropName}; use rustical_xml::{EnumVariants, PropName};
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::str::FromStr; use std::str::FromStr;
@@ -95,7 +95,7 @@ impl DavPushExtension for CalendarResource {
impl Resource for CalendarResource { impl Resource for CalendarResource {
type Prop = CalendarPropWrapper; type Prop = CalendarPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
fn is_collection(&self) -> bool { fn is_collection(&self) -> bool {
true true
@@ -121,7 +121,7 @@ impl Resource for CalendarResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &CalendarPropWrapperName, prop: &CalendarPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
Ok(match prop { Ok(match prop {
@@ -291,8 +291,13 @@ impl Resource for CalendarResource {
Some(&self.cal.principal) Some(&self.cal.principal)
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
if self.cal.subscription_url.is_some() || self.read_only { if self.cal.subscription_url.is_some() {
return Ok(UserPrivilegeSet::owner_write_properties(
user.is_principal(&self.cal.principal),
));
}
if self.read_only {
return Ok(UserPrivilegeSet::owner_read( return Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.cal.principal), user.is_principal(&self.cal.principal),
)); ));

View File

@@ -13,7 +13,7 @@ use axum::handler::Handler;
use axum::response::Response; use axum::response::Response;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{CalendarStore, SubscriptionStore}; use rustical_store::{CalendarStore, SubscriptionStore};
use std::convert::Infallible; use std::convert::Infallible;
use std::sync::Arc; use std::sync::Arc;
@@ -48,7 +48,7 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
type PathComponents = (String, String); // principal, calendar_id type PathComponents = (String, String); // principal, calendar_id
type Resource = CalendarResource; type Resource = CalendarResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy, webdav-push"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy, webdav-push";
@@ -56,8 +56,12 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
async fn get_resource( async fn get_resource(
&self, &self,
(principal, cal_id): &Self::PathComponents, (principal, cal_id): &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Error> { ) -> Result<Self::Resource, Error> {
let calendar = self.cal_store.get_calendar(principal, cal_id).await?; let calendar = self
.cal_store
.get_calendar(principal, cal_id, show_deleted)
.await?;
Ok(CalendarResource { Ok(CalendarResource {
cal: calendar, cal: calendar,
read_only: self.cal_store.is_read_only(cal_id), read_only: self.cal_store.is_read_only(cal_id),

View File

@@ -9,7 +9,7 @@ use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
use http::{HeaderMap, StatusCode}; use http::{HeaderMap, StatusCode};
use rustical_ical::CalendarObject; use rustical_ical::CalendarObject;
use rustical_store::CalendarStore; use rustical_store::CalendarStore;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use std::str::FromStr; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
@@ -21,13 +21,15 @@ pub async fn get_event<C: CalendarStore>(
object_id, object_id,
}): Path<CalendarObjectPathComponents>, }): Path<CalendarObjectPathComponents>,
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>, State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
user: User, user: Principal,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?; let calendar = cal_store
.get_calendar(&principal, &calendar_id, false)
.await?;
if !user.is_principal(&calendar.principal) { if !user.is_principal(&calendar.principal) {
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
@@ -51,7 +53,7 @@ pub async fn put_event<C: CalendarStore>(
object_id, object_id,
}): Path<CalendarObjectPathComponents>, }): Path<CalendarObjectPathComponents>,
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>, State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
user: User, user: Principal,
mut if_none_match: Option<TypedHeader<IfNoneMatch>>, mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
header_map: HeaderMap, header_map: HeaderMap,
body: String, body: String,

View File

@@ -8,7 +8,7 @@ use rustical_dav::{
xml::Resourcetype, xml::Resourcetype,
}; };
use rustical_ical::CalendarObject; use rustical_ical::CalendarObject;
use rustical_store::auth::User; use rustical_store::auth::Principal;
#[derive(Clone, From, Into)] #[derive(Clone, From, Into)]
pub struct CalendarObjectResource { pub struct CalendarObjectResource {
@@ -25,7 +25,7 @@ impl ResourceName for CalendarObjectResource {
impl Resource for CalendarObjectResource { impl Resource for CalendarObjectResource {
type Prop = CalendarObjectPropWrapper; type Prop = CalendarObjectPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
fn is_collection(&self) -> bool { fn is_collection(&self) -> bool {
false false
@@ -38,7 +38,7 @@ impl Resource for CalendarObjectResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &CalendarObjectPropWrapperName, prop: &CalendarObjectPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
Ok(match prop { Ok(match prop {
@@ -81,7 +81,7 @@ impl Resource for CalendarObjectResource {
Some(self.object.get_etag()) Some(self.object.get_etag())
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.principal), user.is_principal(&self.principal),
)) ))

View File

@@ -9,7 +9,7 @@ use async_trait::async_trait;
use axum::{extract::Request, handler::Handler, response::Response}; use axum::{extract::Request, handler::Handler, response::Response};
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::{CalendarStore, auth::User}; use rustical_store::{CalendarStore, auth::Principal};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::{convert::Infallible, sync::Arc}; use std::{convert::Infallible, sync::Arc};
use tower::Service; use tower::Service;
@@ -46,7 +46,7 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
type Resource = CalendarObjectResource; type Resource = CalendarObjectResource;
type MemberType = CalendarObjectResource; type MemberType = CalendarObjectResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
@@ -58,10 +58,11 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
calendar_id, calendar_id,
object_id, object_id,
}: &Self::PathComponents, }: &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Self::Error> { ) -> Result<Self::Resource, Self::Error> {
let object = self let object = self
.cal_store .cal_store
.get_object(principal, calendar_id, object_id, false) .get_object(principal, calendar_id, object_id, show_deleted)
.await?; .await?;
Ok(CalendarObjectResource { Ok(CalendarObjectResource {
object, object,

View File

@@ -6,7 +6,7 @@ use principal::PrincipalResourceService;
use rustical_dav::resource::{PrincipalUri, ResourceService}; use rustical_dav::resource::{PrincipalUri, ResourceService};
use rustical_dav::resources::RootResourceService; use rustical_dav::resources::RootResourceService;
use rustical_store::auth::middleware::AuthenticationLayer; use rustical_store::auth::middleware::AuthenticationLayer;
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use rustical_store::{CalendarStore, SubscriptionStore}; use rustical_store::{CalendarStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
@@ -44,7 +44,7 @@ pub fn caldav_router<AP: AuthenticationProvider, C: CalendarStore, S: Subscripti
Router::new() Router::new()
.nest( .nest(
prefix, prefix,
RootResourceService::<_, User, CalDavPrincipalUri>::new(principal_service.clone()) RootResourceService::<_, Principal, CalDavPrincipalUri>::new(principal_service.clone())
.axum_router() .axum_router()
.layer(AuthenticationLayer::new(auth_provider)) .layer(AuthenticationLayer::new(auth_provider))
.layer(Extension(CalDavPrincipalUri(prefix))), .layer(Extension(CalDavPrincipalUri(prefix))),

View File

@@ -5,7 +5,7 @@ use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{ use rustical_dav::xml::{
GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet, GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet,
}; };
use rustical_store::auth::User; use rustical_store::auth::Principal;
mod service; mod service;
pub use service::*; pub use service::*;
@@ -14,7 +14,7 @@ pub use prop::*;
#[derive(Clone)] #[derive(Clone)]
pub struct PrincipalResource { pub struct PrincipalResource {
principal: User, principal: Principal,
members: Vec<String>, members: Vec<String>,
} }
@@ -27,7 +27,7 @@ impl ResourceName for PrincipalResource {
impl Resource for PrincipalResource { impl Resource for PrincipalResource {
type Prop = PrincipalPropWrapper; type Prop = PrincipalPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
fn is_collection(&self) -> bool { fn is_collection(&self) -> bool {
true true
@@ -48,7 +48,7 @@ impl Resource for PrincipalResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &PrincipalPropWrapperName, prop: &PrincipalPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
let principal_url = puri.principal_uri(&self.principal.id); let principal_url = puri.principal_uri(&self.principal.id);
@@ -113,7 +113,7 @@ impl Resource for PrincipalResource {
Some(&self.principal.id) Some(&self.principal.id)
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_read( Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.principal.id), user.is_principal(&self.principal.id),
)) ))

View File

@@ -2,7 +2,7 @@ use rustical_dav::{
extensions::CommonPropertiesProp, extensions::CommonPropertiesProp,
xml::{GroupMemberSet, GroupMembership, HrefElement, SupportedReportSet}, xml::{GroupMemberSet, GroupMembership, HrefElement, SupportedReportSet},
}; };
use rustical_store::auth::user::PrincipalType; use rustical_store::auth::PrincipalType;
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize}; use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
use strum_macros::VariantArray; use strum_macros::VariantArray;

View File

@@ -5,7 +5,7 @@ use crate::{CalDavPrincipalUri, Error};
use async_trait::async_trait; use async_trait::async_trait;
use axum::Router; use axum::Router;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use rustical_store::{CalendarStore, SubscriptionStore}; use rustical_store::{CalendarStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
@@ -40,7 +40,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
type MemberType = CalendarResource; type MemberType = CalendarResource;
type Resource = PrincipalResource; type Resource = PrincipalResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";
@@ -48,6 +48,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
async fn get_resource( async fn get_resource(
&self, &self,
(principal,): &Self::PathComponents, (principal,): &Self::PathComponents,
_show_deleted: bool,
) -> Result<Self::Resource, Self::Error> { ) -> Result<Self::Resource, Self::Error> {
let user = self let user = self
.auth_provider .auth_provider

View File

@@ -12,7 +12,7 @@ use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::AddressbookStore; use rustical_store::AddressbookStore;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use std::str::FromStr; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
@@ -24,7 +24,7 @@ pub async fn get_object<AS: AddressbookStore>(
object_id, object_id,
}): Path<AddressObjectPathComponents>, }): Path<AddressObjectPathComponents>,
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>, State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
user: User, user: Principal,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
@@ -60,7 +60,7 @@ pub async fn put_object<AS: AddressbookStore>(
object_id, object_id,
}): Path<AddressObjectPathComponents>, }): Path<AddressObjectPathComponents>,
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>, State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
user: User, user: Principal,
mut if_none_match: Option<TypedHeader<IfNoneMatch>>, mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
header_map: HeaderMap, header_map: HeaderMap,
body: String, body: String,

View File

@@ -13,7 +13,7 @@ use rustical_dav::{
xml::Resourcetype, xml::Resourcetype,
}; };
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::auth::User; use rustical_store::auth::Principal;
#[derive(Clone, From, Into)] #[derive(Clone, From, Into)]
pub struct AddressObjectResource { pub struct AddressObjectResource {
@@ -30,7 +30,7 @@ impl ResourceName for AddressObjectResource {
impl Resource for AddressObjectResource { impl Resource for AddressObjectResource {
type Prop = AddressObjectPropWrapper; type Prop = AddressObjectPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
fn is_collection(&self) -> bool { fn is_collection(&self) -> bool {
false false
@@ -43,7 +43,7 @@ impl Resource for AddressObjectResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &AddressObjectPropWrapperName, prop: &AddressObjectPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
Ok(match prop { Ok(match prop {
@@ -78,7 +78,7 @@ impl Resource for AddressObjectResource {
Some(self.object.get_etag()) Some(self.object.get_etag())
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.principal), user.is_principal(&self.principal),
)) ))

View File

@@ -5,7 +5,7 @@ use axum::{extract::Request, handler::Handler, response::Response};
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::{AddressbookStore, auth::User}; use rustical_store::{AddressbookStore, auth::Principal};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::{convert::Infallible, sync::Arc}; use std::{convert::Infallible, sync::Arc};
use tower::Service; use tower::Service;
@@ -37,7 +37,7 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
type Resource = AddressObjectResource; type Resource = AddressObjectResource;
type MemberType = AddressObjectResource; type MemberType = AddressObjectResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook"; const DAV_HEADER: &str = "1, 3, access-control, addressbook";
@@ -49,10 +49,11 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
addressbook_id, addressbook_id,
object_id, object_id,
}: &Self::PathComponents, }: &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Self::Error> { ) -> Result<Self::Resource, Self::Error> {
let object = self let object = self
.addr_store .addr_store
.get_object(principal, addressbook_id, object_id, false) .get_object(principal, addressbook_id, object_id, show_deleted)
.await?; .await?;
Ok(AddressObjectResource { Ok(AddressObjectResource {
object, object,

View File

@@ -10,7 +10,7 @@ use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_dav::privileges::UserPrivilege; use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{AddressbookStore, SubscriptionStore}; use rustical_store::{AddressbookStore, SubscriptionStore};
use std::str::FromStr; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
@@ -19,7 +19,7 @@ use tracing::instrument;
pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>( pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>, State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
user: User, user: Principal,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);

View File

@@ -4,7 +4,7 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use http::StatusCode; use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::User}; use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::Principal};
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument; use tracing::instrument;
@@ -44,7 +44,7 @@ struct MkcolRequest {
#[instrument(skip(addr_store))] #[instrument(skip(addr_store))]
pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>( pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
user: User, user: Principal,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>, State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
body: String, body: String,
) -> Result<Response, Error> { ) -> Result<Response, Error> {

View File

@@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderValue, StatusCode, header};
use rustical_dav::privileges::UserPrivilege; use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_dav_push::register::PushRegister; use rustical_dav_push::register::PushRegister;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{AddressbookStore, Subscription, SubscriptionStore}; use rustical_store::{AddressbookStore, Subscription, SubscriptionStore};
use rustical_xml::XmlDocument; use rustical_xml::XmlDocument;
use tracing::instrument; use tracing::instrument;
@@ -15,7 +15,7 @@ use tracing::instrument;
#[instrument(skip(resource_service))] #[instrument(skip(resource_service))]
pub async fn route_post<AS: AddressbookStore, S: SubscriptionStore>( pub async fn route_post<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addr_id)): Path<(String, String)>, Path((principal, addr_id)): Path<(String, String)>,
user: User, user: Principal,
State(resource_service): State<AddressbookResourceService<AS, S>>, State(resource_service): State<AddressbookResourceService<AS, S>>,
body: String, body: String,
) -> Result<Response, Error> { ) -> Result<Response, Error> {

View File

@@ -9,14 +9,14 @@ use http::StatusCode;
use ical::VcardParser; use ical::VcardParser;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::Addressbook; use rustical_store::Addressbook;
use rustical_store::{AddressbookStore, SubscriptionStore, auth::User}; use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
use tracing::instrument; use tracing::instrument;
#[instrument(skip(addr_store))] #[instrument(skip(addr_store))]
pub async fn route_put<AS: AddressbookStore, S: SubscriptionStore>( pub async fn route_put<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>, State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
user: User, user: Principal,
body: String, body: String,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {

View File

@@ -10,7 +10,7 @@ use rustical_dav::{
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement}, xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},
}; };
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::{AddressbookStore, auth::User}; use rustical_store::{AddressbookStore, auth::Principal};
use rustical_xml::XmlDeserialize; use rustical_xml::XmlDeserialize;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
@@ -63,7 +63,7 @@ pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
prop: &PropfindType<AddressObjectPropWrapperName>, prop: &PropfindType<AddressObjectPropWrapperName>,
path: &str, path: &str,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
addr_store: &AS, addr_store: &AS,

View File

@@ -9,7 +9,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
}; };
use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest}; use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest};
use rustical_store::{AddressbookStore, SubscriptionStore, auth::User}; use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
use rustical_xml::{XmlDeserialize, XmlDocument}; use rustical_xml::{XmlDeserialize, XmlDocument};
use sync_collection::handle_sync_collection; use sync_collection::handle_sync_collection;
use tracing::instrument; use tracing::instrument;
@@ -37,7 +37,7 @@ impl ReportRequest {
#[instrument(skip(addr_store))] #[instrument(skip(addr_store))]
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>( pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
user: User, user: Principal,
OriginalUri(uri): OriginalUri, OriginalUri(uri): OriginalUri,
Extension(puri): Extension<CardDavPrincipalUri>, Extension(puri): Extension<CardDavPrincipalUri>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>, State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,

View File

@@ -13,7 +13,7 @@ use rustical_dav::{
}; };
use rustical_store::{ use rustical_store::{
AddressbookStore, AddressbookStore,
auth::User, auth::Principal,
synctoken::{format_synctoken, parse_synctoken}, synctoken::{format_synctoken, parse_synctoken},
}; };
@@ -21,7 +21,7 @@ pub async fn handle_sync_collection<AS: AddressbookStore>(
sync_collection: &SyncCollectionRequest<AddressObjectPropWrapperName>, sync_collection: &SyncCollectionRequest<AddressObjectPropWrapperName>,
path: &str, path: &str,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
addr_store: &AS, addr_store: &AS,

View File

@@ -10,7 +10,7 @@ use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{Resourcetype, ResourcetypeInner, SupportedReportSet}; use rustical_dav::xml::{Resourcetype, ResourcetypeInner, SupportedReportSet};
use rustical_dav_push::DavPushExtension; use rustical_dav_push::DavPushExtension;
use rustical_store::Addressbook; use rustical_store::Addressbook;
use rustical_store::auth::User; use rustical_store::auth::Principal;
#[derive(Clone, Debug, From, Into)] #[derive(Clone, Debug, From, Into)]
pub struct AddressbookResource(pub(crate) Addressbook); pub struct AddressbookResource(pub(crate) Addressbook);
@@ -36,7 +36,7 @@ impl DavPushExtension for AddressbookResource {
impl Resource for AddressbookResource { impl Resource for AddressbookResource {
type Prop = AddressbookPropWrapper; type Prop = AddressbookPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
fn is_collection(&self) -> bool { fn is_collection(&self) -> bool {
true true
@@ -52,7 +52,7 @@ impl Resource for AddressbookResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &AddressbookPropWrapperName, prop: &AddressbookPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
Ok(match prop { Ok(match prop {
@@ -138,7 +138,7 @@ impl Resource for AddressbookResource {
Some(&self.0.principal) Some(&self.0.principal)
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.0.principal), user.is_principal(&self.0.principal),
)) ))

View File

@@ -14,7 +14,7 @@ use axum::handler::Handler;
use axum::response::Response; use axum::response::Response;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{AddressbookStore, SubscriptionStore}; use rustical_store::{AddressbookStore, SubscriptionStore};
use std::convert::Infallible; use std::convert::Infallible;
use std::sync::Arc; use std::sync::Arc;
@@ -51,7 +51,7 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
type PathComponents = (String, String); // principal, addressbook_id type PathComponents = (String, String); // principal, addressbook_id
type Resource = AddressbookResource; type Resource = AddressbookResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook, webdav-push"; const DAV_HEADER: &str = "1, 3, access-control, addressbook, webdav-push";
@@ -59,10 +59,11 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
async fn get_resource( async fn get_resource(
&self, &self,
(principal, addressbook_id): &Self::PathComponents, (principal, addressbook_id): &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Error> { ) -> Result<Self::Resource, Error> {
let addressbook = self let addressbook = self
.addr_store .addr_store
.get_addressbook(principal, addressbook_id, false) .get_addressbook(principal, addressbook_id, show_deleted)
.await .await
.map_err(|_e| Error::NotFound)?; .map_err(|_e| Error::NotFound)?;
Ok(addressbook.into()) Ok(addressbook.into())

View File

@@ -9,7 +9,7 @@ use rustical_dav::resources::RootResourceService;
use rustical_store::auth::middleware::AuthenticationLayer; use rustical_store::auth::middleware::AuthenticationLayer;
use rustical_store::{ use rustical_store::{
AddressbookStore, SubscriptionStore, AddressbookStore, SubscriptionStore,
auth::{AuthenticationProvider, User}, auth::{AuthenticationProvider, Principal},
}; };
use std::sync::Arc; use std::sync::Arc;
@@ -44,10 +44,12 @@ pub fn carddav_router<AP: AuthenticationProvider, A: AddressbookStore, S: Subscr
Router::new() Router::new()
.nest( .nest(
prefix, prefix,
RootResourceService::<_, User, CardDavPrincipalUri>::new(principal_service.clone()) RootResourceService::<_, Principal, CardDavPrincipalUri>::new(
.axum_router() principal_service.clone(),
.layer(AuthenticationLayer::new(auth_provider)) )
.layer(Extension(CardDavPrincipalUri(prefix))), .axum_router()
.layer(AuthenticationLayer::new(auth_provider))
.layer(Extension(CardDavPrincipalUri(prefix))),
) )
.route( .route(
"/.well-known/carddav", "/.well-known/carddav",

View File

@@ -5,7 +5,7 @@ use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{ use rustical_dav::xml::{
GroupMemberSet, GroupMembership, HrefElement, Resourcetype, ResourcetypeInner, GroupMemberSet, GroupMembership, HrefElement, Resourcetype, ResourcetypeInner,
}; };
use rustical_store::auth::User; use rustical_store::auth::Principal;
mod service; mod service;
pub use service::*; pub use service::*;
@@ -14,7 +14,7 @@ pub use prop::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PrincipalResource { pub struct PrincipalResource {
principal: User, principal: Principal,
members: Vec<String>, members: Vec<String>,
} }
@@ -27,7 +27,7 @@ impl ResourceName for PrincipalResource {
impl Resource for PrincipalResource { impl Resource for PrincipalResource {
type Prop = PrincipalPropWrapper; type Prop = PrincipalPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
fn is_collection(&self) -> bool { fn is_collection(&self) -> bool {
true true
@@ -43,7 +43,7 @@ impl Resource for PrincipalResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &PrincipalPropWrapperName, prop: &PrincipalPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
let principal_href = HrefElement::new(puri.principal_uri(&self.principal.id)); let principal_href = HrefElement::new(puri.principal_uri(&self.principal.id));
@@ -99,7 +99,7 @@ impl Resource for PrincipalResource {
Some(&self.principal.id) Some(&self.principal.id)
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.principal.id), user.is_principal(&self.principal.id),
)) ))

View File

@@ -5,7 +5,7 @@ use crate::{CardDavPrincipalUri, Error};
use async_trait::async_trait; use async_trait::async_trait;
use axum::Router; use axum::Router;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use rustical_store::{AddressbookStore, SubscriptionStore}; use rustical_store::{AddressbookStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
@@ -51,7 +51,7 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
type MemberType = AddressbookResource; type MemberType = AddressbookResource;
type Resource = PrincipalResource; type Resource = PrincipalResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook"; const DAV_HEADER: &str = "1, 3, access-control, addressbook";
@@ -59,6 +59,7 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
async fn get_resource( async fn get_resource(
&self, &self,
(principal,): &Self::PathComponents, (principal,): &Self::PathComponents,
_show_deleted: bool,
) -> Result<Self::Resource, Self::Error> { ) -> Result<Self::Resource, Self::Error> {
let user = self let user = self
.auth_provider .auth_provider

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,6 @@ rustical_dav.workspace = true
rustical_store.workspace = true rustical_store.workspace = true
http.workspace = true http.workspace = true
base64.workspace = true base64.workspace = true
p256.workspace = true
rand.workspace = true rand.workspace = true
ece.workspace = true ece.workspace = true
axum.workspace = true axum.workspace = true

View File

@@ -5,7 +5,7 @@
}, },
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": [
"ES2020", "ES2024",
"DOM", "DOM",
"DOM.Iterable" "DOM.Iterable"
] ]

View File

@@ -1,5 +1,6 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav"; import { createClient } from "webdav";
@customElement("create-addressbook-form") @customElement("create-addressbook-form")
@@ -18,21 +19,24 @@ export class CreateAddressbookForm extends LitElement {
@property() @property()
user: String = '' user: String = ''
@property() @property()
id: String = '' addr_id: String = ''
@property() @property()
displayname: String = '' displayname: String = ''
@property() @property()
description: String = '' description: String = ''
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
override render() { override render() {
return html` return html`
<section> <button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
<h3>Create calendar</h3> <dialog ${ref(this.dialog)}>
<form @submit=${this.submit}> <h3>Create addressbook</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
id 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> </label>
<br> <br>
<label> <label>
@@ -46,15 +50,16 @@ export class CreateAddressbookForm extends LitElement {
</label> </label>
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
</form> </form>
</section> </dialog>
` `
} }
async submit(e: SubmitEvent) { async submit(e: SubmitEvent) {
console.log(this.displayname) console.log(this.displayname)
e.preventDefault() e.preventDefault()
if (!this.id) { if (!this.addr_id) {
alert("Empty id") alert("Empty id")
return return
} }
@@ -63,7 +68,7 @@ export class CreateAddressbookForm extends LitElement {
return return
} }
// TODO: Escape user input: There's not really a security risk here but would be nicer // 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: ` data: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav"> <mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set> <set>

View File

@@ -1,12 +1,12 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav"; import { createClient } from "webdav";
@customElement("create-calendar-form") @customElement("create-calendar-form")
export class CreateCalendarForm extends LitElement { export class CreateCalendarForm extends LitElement {
constructor() { constructor() {
super() super()
} }
protected override createRenderRoot() { protected override createRenderRoot() {
@@ -18,7 +18,7 @@ export class CreateCalendarForm extends LitElement {
@property() @property()
user: String = '' user: String = ''
@property() @property()
id: String = '' cal_id: String = ''
@property() @property()
displayname: String = '' displayname: String = ''
@property() @property()
@@ -30,15 +30,19 @@ export class CreateCalendarForm extends LitElement {
@property() @property()
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set() components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
override render() { override render() {
return html` return html`
<section> <button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${ref(this.dialog)}>
<h3>Create calendar</h3> <h3>Create calendar</h3>
<form @submit=${this.submit}> <form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
id 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> </label>
<br> <br>
<label> <label>
@@ -69,15 +73,16 @@ export class CreateCalendarForm extends LitElement {
`)} `)}
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
</form> <button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
</section> </form>
` </dialog>
`
} }
async submit(e: SubmitEvent) { async submit(e: SubmitEvent) {
console.log(this.displayname) console.log(this.displayname)
e.preventDefault() e.preventDefault()
if (!this.id) { if (!this.cal_id) {
alert("Empty id") alert("Empty id")
return return
} }
@@ -89,7 +94,7 @@ export class CreateCalendarForm extends LitElement {
alert("No calendar components selected") 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: ` 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/"> <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> <set>
@@ -97,7 +102,7 @@ export class CreateCalendarForm extends LitElement {
<displayname>${this.displayname}</displayname> <displayname>${this.displayname}</displayname>
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''} ${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''} ${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''}
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ''} ${this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ''}
<CAL:supported-calendar-component-set> <CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')} ${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')}
</CAL:supported-calendar-component-set> </CAL:supported-calendar-component-set>

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,10 +1,13 @@
{ {
"module": "nodenext",
"moduleResolution": "nodenext",
"compilerOptions": { "compilerOptions": {
"target": "es2020", "target": "es2024",
"moduleResolution": "bundler",
"experimentalDecorators": true, "experimentalDecorators": true,
"useDefineForClassFields": false "useDefineForClassFields": false,
"lib": [
"dom",
"es2024"
]
}, },
"include": [ "include": [
"lib/**/*.ts" "lib/**/*.ts"

View File

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

View File

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

View File

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

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

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
};

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; display: grid;
min-height: 80px; min-height: 80px;
grid-template-areas: grid-template-areas:
". . color-chip" ". . color-chip"
"title comps color-chip" "title comps color-chip"
"description . color-chip" "description description color-chip"
"subscription-url . color-chip" "subscription-url subscription-url color-chip"
"actions . color-chip" "actions actions color-chip"
". . color-chip"; ". . color-chip";
grid-template-rows: 12px auto auto auto auto 12px; grid-template-rows: 12px auto auto auto auto 12px;
grid-template-columns: min-content auto 80px; grid-template-columns: min-content auto 80px;
row-gap: 4px;
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
padding-left: 12px; padding-left: 12px;
@@ -220,6 +221,8 @@ table {
.actions { .actions {
grid-area: actions; grid-area: actions;
width: fit-content; width: fit-content;
display: flex;
gap: 12px;
} }
&:hover { &:hover {

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
{% block imports %} {% block imports %}
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script> <script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script> <script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -67,7 +68,7 @@
<h2>Calendars</h2> <h2>Calendars</h2>
<ul> <ul>
{% for calendar in calendars %} {% 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 }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"> <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">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
@@ -83,9 +84,12 @@
<span class="subscription-url">{{ subscription_url }}</span> <span class="subscription-url">{{ subscription_url }}</span>
{% endif %} {% endif %}
<div class="actions"> <div class="actions">
<form action="/caldav/principal/{{ calendar.principal }}/calendar/{{ calendar.id }}" target="_blank" method="GET"> <form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
<button type="submit">Download</button> <button type="submit">Download</button>
</form> </form>
{% if !calendar.id.starts_with("_birthdays_") %}
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
{% endif %}
</div> </div>
<div class="color-chip"></div> <div class="color-chip"></div>
</a> </a>
@@ -98,7 +102,7 @@
<h3>Deleted Calendars</h3> <h3>Deleted Calendars</h3>
<ul> <ul>
{% for calendar in deleted_calendars %} {% 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 }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"> <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">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
@@ -114,6 +118,7 @@
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form"> <form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button> <button type="submit">Restore</button>
</form> </form>
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
</div> </div>
<div class="color-chip"></div> <div class="color-chip"></div>
</a> </a>
@@ -139,6 +144,7 @@
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET"> <form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET">
<button type="submit">Download</button> <button type="submit">Download</button>
</form> </form>
<delete-button trash href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
</div> </div>
</a> </a>
</li> </li>
@@ -160,6 +166,7 @@
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form"> <form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button> <button type="submit">Restore</button>
</form> </form>
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
</div> </div>
</a> </a>
</li> </li>

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ use axum_extra::{TypedHeader, extract::Host};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use headers::UserAgent; use headers::UserAgent;
use http::StatusCode; use http::StatusCode;
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use tracing::instrument; use tracing::instrument;
@@ -101,7 +101,7 @@ struct NextcloudLoginPage {
pub(crate) async fn get_nextcloud_flow( pub(crate) async fn get_nextcloud_flow(
Extension(state): Extension<Arc<NextcloudFlows>>, Extension(state): Extension<Arc<NextcloudFlows>>,
Path(flow_id): Path<String>, Path(flow_id): Path<String>,
user: User, user: Principal,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if let Some(flow) = state.flows.read().await.get(&flow_id) { if let Some(flow) = state.flows.read().await.get(&flow_id) {
Ok(Html( Ok(Html(
@@ -131,7 +131,7 @@ struct NextcloudLoginSuccessPage {
#[instrument(skip(state))] #[instrument(skip(state))]
pub(crate) async fn post_nextcloud_flow( pub(crate) async fn post_nextcloud_flow(
user: User, user: Principal,
Extension(state): Extension<Arc<NextcloudFlows>>, Extension(state): Extension<Arc<NextcloudFlows>>,
Path(flow_id): Path<String>, Path(flow_id): Path<String>,
Host(host): Host, Host(host): Host,

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use rustical_oidc::UserStore; use rustical_oidc::UserStore;
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
pub struct OidcUserStore<AP: AuthenticationProvider>(pub Arc<AP>); pub struct OidcUserStore<AP: AuthenticationProvider>(pub Arc<AP>);
@@ -23,7 +23,7 @@ impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
async fn insert_user(&self, id: &str) -> Result<(), Self::Error> { async fn insert_user(&self, id: &str) -> Result<(), Self::Error> {
self.0 self.0
.insert_principal( .insert_principal(
User { Principal {
id: id.to_owned(), id: id.to_owned(),
displayname: None, displayname: None,
principal_type: Default::default(), principal_type: Default::default(),

View File

@@ -10,7 +10,7 @@ use axum::{
use axum_extra::TypedHeader; use axum_extra::TypedHeader;
use headers::Referer; use headers::Referer;
use http::StatusCode; use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, auth::User}; use rustical_store::{Addressbook, AddressbookStore, auth::Principal};
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "pages/addressbook.html")] #[template(path = "pages/addressbook.html")]
@@ -21,7 +21,7 @@ struct AddressbookPage {
pub async fn route_addressbook<AS: AddressbookStore>( pub async fn route_addressbook<AS: AddressbookStore>(
Path((owner, addrbook_id)): Path<(String, String)>, Path((owner, addrbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>, Extension(store): Extension<Arc<AS>>,
user: User, user: Principal,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response()); return Ok(StatusCode::UNAUTHORIZED.into_response());
@@ -35,7 +35,7 @@ pub async fn route_addressbook<AS: AddressbookStore>(
pub async fn route_addressbook_restore<AS: AddressbookStore>( pub async fn route_addressbook_restore<AS: AddressbookStore>(
Path((owner, addressbook_id)): Path<(String, String)>, Path((owner, addressbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>, Extension(store): Extension<Arc<AS>>,
user: User, user: Principal,
referer: Option<TypedHeader<Referer>>, referer: Option<TypedHeader<Referer>>,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
@@ -47,19 +47,3 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
None => (StatusCode::CREATED, "Restored").into_response(), None => (StatusCode::CREATED, "Restored").into_response(),
}) })
} }
pub async fn route_delete_addressbook<AS: AddressbookStore>(
Path((owner, addressbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>,
user: User,
) -> 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

@@ -12,7 +12,7 @@ use headers::{ContentType, HeaderMapExt};
use http::{HeaderValue, StatusCode, header}; use http::{HeaderValue, StatusCode, header};
use percent_encoding::{CONTROLS, utf8_percent_encode}; use percent_encoding::{CONTROLS, utf8_percent_encode};
use rand::{Rng, distr::Alphanumeric}; use rand::{Rng, distr::Alphanumeric};
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
@@ -47,7 +47,7 @@ pub(crate) struct PostAppTokenForm {
} }
pub async fn route_post_app_token<AP: AuthenticationProvider>( pub async fn route_post_app_token<AP: AuthenticationProvider>(
user: User, user: Principal,
Extension(auth_provider): Extension<Arc<AP>>, Extension(auth_provider): Extension<Arc<AP>>,
Path(user_id): Path<String>, Path(user_id): Path<String>,
Host(hostname): Host, Host(hostname): Host,
@@ -96,7 +96,7 @@ pub async fn route_post_app_token<AP: AuthenticationProvider>(
} }
pub async fn route_delete_app_token<AP: AuthenticationProvider>( pub async fn route_delete_app_token<AP: AuthenticationProvider>(
user: User, user: Principal,
Extension(auth_provider): Extension<Arc<AP>>, Extension(auth_provider): Extension<Arc<AP>>,
Path((user_id, token_id)): Path<(String, String)>, Path((user_id, token_id)): Path<(String, String)>,
) -> Result<Redirect, rustical_store::Error> { ) -> Result<Redirect, rustical_store::Error> {

View File

@@ -10,7 +10,7 @@ use axum::{
use axum_extra::TypedHeader; use axum_extra::TypedHeader;
use headers::Referer; use headers::Referer;
use http::StatusCode; use http::StatusCode;
use rustical_store::{Calendar, CalendarStore, auth::User}; use rustical_store::{Calendar, CalendarStore, auth::Principal};
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "pages/calendar.html")] #[template(path = "pages/calendar.html")]
@@ -21,13 +21,13 @@ struct CalendarPage {
pub async fn route_calendar<C: CalendarStore>( pub async fn route_calendar<C: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>, Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<C>>, Extension(store): Extension<Arc<C>>,
user: User, user: Principal,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response()); return Ok(StatusCode::UNAUTHORIZED.into_response());
} }
Ok(CalendarPage { Ok(CalendarPage {
calendar: store.get_calendar(&owner, &cal_id).await?, calendar: store.get_calendar(&owner, &cal_id, true).await?,
} }
.into_response()) .into_response())
} }
@@ -35,7 +35,7 @@ pub async fn route_calendar<C: CalendarStore>(
pub async fn route_calendar_restore<CS: CalendarStore>( pub async fn route_calendar_restore<CS: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>, Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<CS>>, Extension(store): Extension<Arc<CS>>,
user: User, user: Principal,
referer: Option<TypedHeader<Referer>>, referer: Option<TypedHeader<Referer>>,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
@@ -47,17 +47,3 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
None => (StatusCode::CREATED, "Restored").into_response(), None => (StatusCode::CREATED, "Restored").into_response(),
}) })
} }
pub async fn route_delete_calendar<C: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<C>>,
user: User,
) -> 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

@@ -12,13 +12,13 @@ use headers::UserAgent;
use http::StatusCode; use http::StatusCode;
use rustical_store::{ use rustical_store::{
Addressbook, AddressbookStore, Calendar, CalendarStore, Addressbook, AddressbookStore, Calendar, CalendarStore,
auth::{AuthenticationProvider, User, user::AppToken}, auth::{AppToken, AuthenticationProvider, Principal},
}; };
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "pages/user.html")] #[template(path = "pages/user.html")]
pub struct UserPage { pub struct UserPage {
pub user: User, pub user: Principal,
pub app_tokens: Vec<AppToken>, pub app_tokens: Vec<AppToken>,
pub calendars: Vec<Calendar>, pub calendars: Vec<Calendar>,
pub deleted_calendars: Vec<Calendar>, pub deleted_calendars: Vec<Calendar>,
@@ -39,7 +39,7 @@ pub async fn route_user_named<
Extension(auth_provider): Extension<Arc<AP>>, Extension(auth_provider): Extension<Arc<AP>>,
TypedHeader(user_agent): TypedHeader<UserAgent>, TypedHeader(user_agent): TypedHeader<UserAgent>,
Host(host): Host, Host(host): Host,
user: User, user: Principal,
) -> impl IntoResponse { ) -> impl IntoResponse {
if user_id != user.id { if user_id != user.id {
return StatusCode::UNAUTHORIZED.into_response(); return StatusCode::UNAUTHORIZED.into_response();
@@ -81,11 +81,11 @@ pub async fn route_user_named<
.into_response() .into_response()
} }
pub async fn route_get_home(user: User) -> Redirect { pub async fn route_get_home(user: Principal) -> Redirect {
Redirect::to(&format!("/frontend/user/{}", user.id)) Redirect::to(&format!("/frontend/user/{}", user.id))
} }
pub async fn route_root(user: Option<User>) -> Redirect { pub async fn route_root(user: Option<Principal>) -> Redirect {
match user { match user {
Some(user) => route_get_home(user).await, Some(user) => route_get_home(user).await,
None => Redirect::to("/frontend/login"), None => Redirect::to("/frontend/login"),

View File

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

View File

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

View File

@@ -138,7 +138,8 @@ pub async fn route_post_oidc(
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct AuthCallbackQuery { pub struct AuthCallbackQuery {
code: AuthorizationCode, code: AuthorizationCode,
iss: IssuerUrl, // RFC 9207
iss: Option<IssuerUrl>,
state: String, state: String,
} }
@@ -153,7 +154,9 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
) -> Result<Response, OidcError> { ) -> Result<Response, OidcError> {
let callback_uri = format!("https://{host}/frontend/login/oidc/callback"); let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
assert_eq!(iss, oidc_config.issuer); if let Some(iss) = iss {
assert_eq!(iss, oidc_config.issuer);
}
let oidc_state = session let oidc_state = session
.remove::<OidcState>(SESSION_KEY_OIDC_STATE) .remove::<OidcState>(SESSION_KEY_OIDC_STATE)
.await? .await?

View File

@@ -1,17 +1,26 @@
pub mod middleware; pub mod middleware;
pub mod user; mod principal;
use crate::error::Error; use crate::error::Error;
use async_trait::async_trait; use async_trait::async_trait;
pub use principal::{AppToken, Principal, PrincipalType};
#[async_trait] #[async_trait]
pub trait AuthenticationProvider: Send + Sync + 'static { pub trait AuthenticationProvider: Send + Sync + 'static {
async fn get_principals(&self) -> Result<Vec<User>, crate::Error>; async fn get_principals(&self) -> Result<Vec<Principal>, crate::Error>;
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>; async fn get_principal(&self, id: &str) -> Result<Option<Principal>, crate::Error>;
async fn remove_principal(&self, id: &str) -> Result<(), crate::Error>; async fn remove_principal(&self, id: &str) -> Result<(), crate::Error>;
async fn insert_principal(&self, user: User, overwrite: bool) -> Result<(), crate::Error>; async fn insert_principal(&self, user: Principal, overwrite: bool) -> Result<(), crate::Error>;
async fn validate_password(&self, user_id: &str, password: &str) async fn validate_password(
-> Result<Option<User>, Error>; &self,
async fn validate_app_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error>; user_id: &str,
password: &str,
) -> Result<Option<Principal>, Error>;
async fn validate_app_token(
&self,
user_id: &str,
token: &str,
) -> Result<Option<Principal>, Error>;
/// Returns a token identifier /// Returns a token identifier
async fn add_app_token( async fn add_app_token(
&self, &self,
@@ -28,5 +37,3 @@ pub trait AuthenticationProvider: Send + Sync + 'static {
} }
pub use middleware::AuthenticationMiddleware; pub use middleware::AuthenticationMiddleware;
use user::AppToken;
pub use user::User;

View File

@@ -78,8 +78,7 @@ pub struct AppToken {
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
// TODO: Rename this to Principal pub struct Principal {
pub struct User {
pub id: String, pub id: String,
pub displayname: Option<String>, pub displayname: Option<String>,
#[serde(default)] #[serde(default)]
@@ -89,7 +88,7 @@ pub struct User {
pub memberships: Vec<String>, pub memberships: Vec<String>,
} }
impl User { impl Principal {
/// Returns true if the user is either /// Returns true if the user is either
/// - the principal itself /// - the principal itself
/// - has full access to the prinicpal (is member) /// - has full access to the prinicpal (is member)
@@ -114,7 +113,7 @@ impl User {
} }
} }
impl rustical_dav::Principal for User { impl rustical_dav::Principal for Principal {
fn get_id(&self) -> &str { fn get_id(&self) -> &str {
&self.id &self.id
} }
@@ -134,7 +133,7 @@ impl IntoResponse for UnauthorizedError {
} }
} }
impl<S: Send + Sync + Clone> FromRequestParts<S> for User { impl<S: Send + Sync + Clone> FromRequestParts<S> for Principal {
type Rejection = UnauthorizedError; type Rejection = UnauthorizedError;
async fn from_request_parts( async fn from_request_parts(
@@ -149,7 +148,7 @@ impl<S: Send + Sync + Clone> FromRequestParts<S> for User {
} }
} }
impl<S: Send + Sync + Clone> OptionalFromRequestParts<S> for User { impl<S: Send + Sync + Clone> OptionalFromRequestParts<S> for Principal {
type Rejection = Infallible; type Rejection = Infallible;
async fn from_request_parts( async fn from_request_parts(

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ use pbkdf2::{
}; };
use rustical_store::{ use rustical_store::{
Error, Secret, Error, Secret,
auth::{AuthenticationProvider, User, user::AppToken}, auth::{AppToken, AuthenticationProvider, Principal},
}; };
use sqlx::{SqlitePool, types::Json}; use sqlx::{SqlitePool, types::Json};
use tracing::instrument; use tracing::instrument;
@@ -21,11 +21,11 @@ struct PrincipalRow {
memberships: Option<Json<Vec<Option<String>>>>, memberships: Option<Json<Vec<Option<String>>>>,
} }
impl TryFrom<PrincipalRow> for User { impl TryFrom<PrincipalRow> for Principal {
type Error = Error; type Error = Error;
fn try_from(value: PrincipalRow) -> Result<Self, Self::Error> { fn try_from(value: PrincipalRow) -> Result<Self, Self::Error> {
Ok(User { Ok(Principal {
id: value.id, id: value.id,
displayname: value.displayname, displayname: value.displayname,
password: value.password_hash.map(Secret::from), password: value.password_hash.map(Secret::from),
@@ -49,8 +49,8 @@ pub struct SqlitePrincipalStore {
#[async_trait] #[async_trait]
impl AuthenticationProvider for SqlitePrincipalStore { impl AuthenticationProvider for SqlitePrincipalStore {
#[instrument] #[instrument]
async fn get_principals(&self) -> Result<Vec<User>, Error> { async fn get_principals(&self) -> Result<Vec<Principal>, Error> {
let result: Result<Vec<User>, Error> = sqlx::query_as!( let result: Result<Vec<Principal>, Error> = sqlx::query_as!(
PrincipalRow, PrincipalRow,
r#" r#"
SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS "memberships: Json<Vec<Option<String>>>" SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS "memberships: Json<Vec<Option<String>>>"
@@ -63,13 +63,13 @@ impl AuthenticationProvider for SqlitePrincipalStore {
.await .await
.map_err(crate::Error::from)? .map_err(crate::Error::from)?
.into_iter() .into_iter()
.map(User::try_from) .map(Principal::try_from)
.collect(); .collect();
Ok(result?) Ok(result?)
} }
#[instrument] #[instrument]
async fn get_principal(&self, id: &str) -> Result<Option<User>, Error> { async fn get_principal(&self, id: &str) -> Result<Option<Principal>, Error> {
let row= sqlx::query_as!( let row= sqlx::query_as!(
PrincipalRow, PrincipalRow,
r#" r#"
@@ -83,7 +83,7 @@ impl AuthenticationProvider for SqlitePrincipalStore {
.fetch_optional(&self.db) .fetch_optional(&self.db)
.await .await
.map_err(crate::Error::from)? .map_err(crate::Error::from)?
.map(User::try_from); .map(Principal::try_from);
if let Some(row) = row { if let Some(row) = row {
Ok(Some(row?)) Ok(Some(row?))
} else { } else {
@@ -103,7 +103,7 @@ impl AuthenticationProvider for SqlitePrincipalStore {
#[instrument] #[instrument]
async fn insert_principal( async fn insert_principal(
&self, &self,
user: User, user: Principal,
overwrite: bool, overwrite: bool,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
// Would be cleaner to put this into a transaction but for now it will be fine // Would be cleaner to put this into a transaction but for now it will be fine
@@ -142,7 +142,11 @@ impl AuthenticationProvider for SqlitePrincipalStore {
} }
#[instrument(skip(token))] #[instrument(skip(token))]
async fn validate_app_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error> { async fn validate_app_token(
&self,
user_id: &str,
token: &str,
) -> Result<Option<Principal>, Error> {
for app_token in &self.get_app_tokens(user_id).await? { for app_token in &self.get_app_tokens(user_id).await? {
if password_auth::verify_password(token, app_token.token.as_ref()).is_ok() { if password_auth::verify_password(token, app_token.token.as_ref()).is_ok() {
return self.get_principal(user_id).await; return self.get_principal(user_id).await;
@@ -169,8 +173,8 @@ impl AuthenticationProvider for SqlitePrincipalStore {
&self, &self,
user_id: &str, user_id: &str,
password_input: &str, password_input: &str,
) -> Result<Option<User>, Error> { ) -> Result<Option<Principal>, Error> {
let user: User = match self.get_principal(user_id).await? { let user: Principal = match self.get_principal(user_id).await? {
Some(user) => user, Some(user) => user,
None => return Ok(None), None => return Ok(None),
}; };

View File

@@ -4,7 +4,8 @@ a CalDAV/CardDAV server
!!! warning !!! warning
RustiCal is **not production-ready!** RustiCal is **not production-ready!**
While I've started migrating to RustiCal and becoming more confident, please know that bugs and rough edges will still occur. 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. :) 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 ## Features
@@ -25,3 +26,4 @@ If you still want to play around with it in its current state, absolutely feel f
- GNOME Accounts, GNOME Calendar, GNOME Contacts - GNOME Accounts, GNOME Calendar, GNOME Contacts
- Evolution - Evolution
- Apple Calendar - Apple Calendar
- Home Assistant integration

View File

@@ -6,7 +6,7 @@ use figment::{
providers::{Env, Format, Toml}, providers::{Env, Format, Toml},
}; };
use password_hash::{PasswordHasher, SaltString, rand_core::OsRng}; use password_hash::{PasswordHasher, SaltString, rand_core::OsRng};
use rustical_store::auth::{AuthenticationProvider, User, user::PrincipalType}; use rustical_store::auth::{AuthenticationProvider, Principal, PrincipalType};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
pub struct PrincipalsArgs { pub struct PrincipalsArgs {
@@ -99,7 +99,7 @@ pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> {
}; };
principal_store principal_store
.insert_principal( .insert_principal(
User { Principal {
id, id,
displayname: name, displayname: name,
principal_type: principal_type.unwrap_or_default(), principal_type: principal_type.unwrap_or_default(),

View File

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