mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 19:22:26 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
786b15f5b9 | ||
|
|
f5d097ac55 | ||
|
|
668fa86e3c | ||
|
|
23d2024644 | ||
|
|
15aadcf1be | ||
|
|
4a3b7d7ce6 | ||
|
|
1a2f3b8f8a | ||
|
|
9e8c218308 | ||
|
|
f2adce739b | ||
|
|
0415664ff3 | ||
|
|
677e0082fa | ||
|
|
a387885b0a | ||
|
|
990b953055 | ||
|
|
36b47a645d | ||
|
|
aa02d11f58 | ||
|
|
1c31323512 | ||
|
|
03ae492483 | ||
|
|
0c48507f0c | ||
|
|
829d4a4385 | ||
|
|
4fe28c5b0f | ||
|
|
529f36ad99 | ||
|
|
ca5891314c | ||
|
|
e653c68cae | ||
|
|
26941c621b | ||
|
|
86ab6ef75e |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) or ?)",
|
"query": "SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -22,5 +22,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "395e40a7b3333b79bc2ad50a123d99f74bc2712a16257ee2119dd211fdb61f7e"
|
"hash": "246ec675667992c1297c29348d46496a884c59adb8b64b569d36f4ce10f88f47"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?)",
|
"query": "SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 3
|
"Right": 4
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "d2f7423e2e8f97607f6664200990dcadb927445880ec6edffba3b5aedf4e199b"
|
"hash": "543838c030550cb09d1af08adfeade8b7ce3575d92fddbc6e9582d141bc9e49d"
|
||||||
}
|
}
|
||||||
121
Cargo.lock
generated
121
Cargo.lock
generated
@@ -759,6 +759,23 @@ dependencies = [
|
|||||||
"spki",
|
"spki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ece"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2ea1d2f2cc974957a4e2575d8e5bb494549bab66338d6320c2789abcfff5746"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"byteorder",
|
||||||
|
"hex",
|
||||||
|
"hkdf",
|
||||||
|
"lazy_static",
|
||||||
|
"once_cell",
|
||||||
|
"openssl",
|
||||||
|
"sha2",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ed25519"
|
name = "ed25519"
|
||||||
version = "2.2.3"
|
version = "2.2.3"
|
||||||
@@ -903,6 +920,21 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||||
|
dependencies = [
|
||||||
|
"foreign-types-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -1622,6 +1654,18 @@ version = "0.8.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit-serde"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/lennart-k/matchit-serde?rev=f0591d13#f0591d139ea1c88fa4ee397f3fcb4225fad4c6dc"
|
||||||
|
dependencies = [
|
||||||
|
"derive_more",
|
||||||
|
"matchit",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -1809,6 +1853,54 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.73"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-src"
|
||||||
|
version = "300.5.0+3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.109"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry"
|
name = "opentelemetry"
|
||||||
version = "0.30.0"
|
version = "0.30.0"
|
||||||
@@ -2644,7 +2736,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical"
|
name = "rustical"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -2687,7 +2779,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_caldav"
|
name = "rustical_caldav"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2722,7 +2814,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_carddav"
|
name = "rustical_carddav"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2754,7 +2846,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav"
|
name = "rustical_dav"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2765,6 +2857,8 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"log",
|
"log",
|
||||||
|
"matchit",
|
||||||
|
"matchit-serde",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rustical_xml",
|
"rustical_xml",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2777,15 +2871,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav_push"
|
name = "rustical_dav_push"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"axum",
|
||||||
|
"base64 0.22.1",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
"ece",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"log",
|
"log",
|
||||||
|
"openssl",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
|
"rand 0.9.1",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rustical_dav",
|
"rustical_dav",
|
||||||
"rustical_store",
|
"rustical_store",
|
||||||
@@ -2798,7 +2897,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_frontend"
|
name = "rustical_frontend"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_web",
|
"askama_web",
|
||||||
@@ -2831,7 +2930,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_ical"
|
name = "rustical_ical"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2849,7 +2948,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_oidc"
|
name = "rustical_oidc"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2864,7 +2963,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store"
|
name = "rustical_store"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -2898,7 +2997,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store_sqlite"
|
name = "rustical_store_sqlite"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2918,7 +3017,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_xml"
|
name = "rustical_xml"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.3.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"
|
||||||
@@ -34,6 +34,7 @@ opentelemetry = [
|
|||||||
debug = 0
|
debug = 0
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
matchit = "0.8"
|
||||||
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
|
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
@@ -133,6 +134,11 @@ reqwest = { version = "0.12", features = [
|
|||||||
], default-features = false }
|
], default-features = false }
|
||||||
openidconnect = "4.0"
|
openidconnect = "4.0"
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
|
matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "f0591d13" }
|
||||||
|
ece = { version = "2.3", default-features = false, features = [
|
||||||
|
"backend-openssl",
|
||||||
|
] }
|
||||||
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_store = { workspace = true }
|
rustical_store = { workspace = true }
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ RUN case $TARGETPLATFORM in \
|
|||||||
*) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \
|
*) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \
|
||||||
esac
|
esac
|
||||||
|
|
||||||
RUN apk add --no-cache musl-dev llvm19 clang \
|
RUN apk add --no-cache musl-dev llvm19 clang perl pkgconf make \
|
||||||
&& rustup target add "$(cat /tmp/rust_target)" \
|
&& rustup target add "$(cat /tmp/rust_target)" \
|
||||||
&& cargo install cargo-chef --locked \
|
&& cargo install cargo-chef --locked \
|
||||||
&& rm -rf "$CARGO_HOME/registry"
|
&& rm -rf "$CARGO_HOME/registry"
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ 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
|
||||||
|
|
||||||
- easy to backup, everything saved in one SQLite database
|
- easy to backup, everything saved in one SQLite database
|
||||||
- ~~[WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5~~ (currently broken)
|
- also export feature in the frontend
|
||||||
|
- [WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5
|
||||||
- lightweight (the container image contains only one binary)
|
- lightweight (the container image contains only one binary)
|
||||||
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
||||||
- deleted calendars are recoverable
|
- deleted calendars are recoverable
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ accepted = [
|
|||||||
"CDLA-Permissive-2.0",
|
"CDLA-Permissive-2.0",
|
||||||
"Zlib",
|
"Zlib",
|
||||||
"AGPL-3.0",
|
"AGPL-3.0",
|
||||||
|
"MPL-2.0",
|
||||||
]
|
]
|
||||||
workarounds = ["ring", "chrono", "rustls"]
|
workarounds = ["ring", "chrono", "rustls"]
|
||||||
|
|||||||
@@ -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,7 +18,7 @@ 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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pub mod mkcalendar;
|
|
||||||
// pub mod post;
|
|
||||||
pub mod get;
|
pub mod get;
|
||||||
|
pub mod mkcalendar;
|
||||||
|
pub mod post;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::calendar::resource::{CalendarResource, CalendarResourceService};
|
use crate::calendar::CalendarResourceService;
|
||||||
|
use crate::calendar::resource::CalendarResource;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use http::{HeaderMap, StatusCode, header};
|
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;
|
||||||
@@ -14,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> {
|
||||||
@@ -73,20 +74,17 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
.upsert_subscription(subscription)
|
.upsert_subscription(subscription)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// let location = req
|
// TODO: make nicer
|
||||||
// .resource_map()
|
let location = format!("/push_subscription/{sub_id}");
|
||||||
// .url_for(&req, "subscription", &[sub_id])
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
let location = "asd";
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
HeaderMap::from_iter([(header::LOCATION, location)]),
|
HeaderMap::from_iter([
|
||||||
|
(header::LOCATION, HeaderValue::from_str(&location).unwrap()),
|
||||||
|
(
|
||||||
|
header::EXPIRES,
|
||||||
|
HeaderValue::from_str(&expires.to_rfc2822()).unwrap(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
.into_response());
|
.into_response())
|
||||||
|
|
||||||
Ok(HttpResponse::Created()
|
|
||||||
.append_header((header::LOCATION, location.to_string()))
|
|
||||||
.append_header((header::EXPIRES, expires.to_rfc2822()))
|
|
||||||
.finish())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
|||||||
if let Some(filename) = href.strip_prefix(path) {
|
if let Some(filename) = href.strip_prefix(path) {
|
||||||
let filename = filename.trim_start_matches("/");
|
let filename = filename.trim_start_matches("/");
|
||||||
if let Some(object_id) = filename.strip_suffix(".ics") {
|
if let Some(object_id) = filename.strip_suffix(".ics") {
|
||||||
match store.get_object(principal, cal_id, object_id).await {
|
match store.get_object(principal, cal_id, object_id, false).await {
|
||||||
Ok(object) => result.push(object),
|
Ok(object) => result.push(object),
|
||||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ use rustical_dav::extensions::{
|
|||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
use rustical_dav::privileges::UserPrivilegeSet;
|
||||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||||
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet};
|
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet};
|
||||||
use rustical_dav_push::DavPushExtension;
|
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;
|
||||||
@@ -58,7 +58,7 @@ pub enum CalendarProp {
|
|||||||
pub enum CalendarPropWrapper {
|
pub enum CalendarPropWrapper {
|
||||||
Calendar(CalendarProp),
|
Calendar(CalendarProp),
|
||||||
SyncToken(SyncTokenExtensionProp),
|
SyncToken(SyncTokenExtensionProp),
|
||||||
// DavPush(DavPushExtensionProp),
|
DavPush(DavPushExtensionProp),
|
||||||
Common(CommonPropertiesProp),
|
Common(CommonPropertiesProp),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +95,11 @@ 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;
|
||||||
|
|
||||||
const IS_COLLECTION: bool = true;
|
fn is_collection(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn get_resourcetype(&self) -> Resourcetype {
|
fn get_resourcetype(&self) -> Resourcetype {
|
||||||
if self.cal.subscription_url.is_none() {
|
if self.cal.subscription_url.is_none() {
|
||||||
@@ -119,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 {
|
||||||
@@ -166,9 +168,9 @@ impl Resource for CalendarResource {
|
|||||||
CalendarPropWrapperName::SyncToken(prop) => {
|
CalendarPropWrapperName::SyncToken(prop) => {
|
||||||
CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?)
|
CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?)
|
||||||
}
|
}
|
||||||
// CalendarPropWrapperName::DavPush(prop) => {
|
CalendarPropWrapperName::DavPush(prop) => {
|
||||||
// CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?)
|
CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?)
|
||||||
// }
|
}
|
||||||
CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common(
|
CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common(
|
||||||
CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
|
CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
|
||||||
),
|
),
|
||||||
@@ -226,7 +228,7 @@ impl Resource for CalendarResource {
|
|||||||
CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly),
|
CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||||
},
|
},
|
||||||
CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop),
|
CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop),
|
||||||
// CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop),
|
CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop),
|
||||||
CalendarPropWrapper::Common(prop) => CommonPropertiesExtension::set_prop(self, prop),
|
CalendarPropWrapper::Common(prop) => CommonPropertiesExtension::set_prop(self, prop),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,7 +272,7 @@ impl Resource for CalendarResource {
|
|||||||
CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly),
|
CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly),
|
||||||
},
|
},
|
||||||
CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop),
|
CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop),
|
||||||
// CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop),
|
CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop),
|
||||||
CalendarPropWrapperName::Common(prop) => {
|
CalendarPropWrapperName::Common(prop) => {
|
||||||
CommonPropertiesExtension::remove_prop(self, prop)
|
CommonPropertiesExtension::remove_prop(self, prop)
|
||||||
}
|
}
|
||||||
@@ -289,7 +291,7 @@ 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() || 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),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::calendar::methods::get::route_get;
|
use crate::calendar::methods::get::route_get;
|
||||||
use crate::calendar::methods::mkcalendar::route_mkcalendar;
|
use crate::calendar::methods::mkcalendar::route_mkcalendar;
|
||||||
|
use crate::calendar::methods::post::route_post;
|
||||||
use crate::calendar::methods::report::route_report_calendar;
|
use crate::calendar::methods::report::route_report_calendar;
|
||||||
use crate::calendar::resource::CalendarResource;
|
use crate::calendar::resource::CalendarResource;
|
||||||
use crate::calendar_object::CalendarObjectResourceService;
|
use crate::calendar_object::CalendarObjectResourceService;
|
||||||
@@ -12,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;
|
||||||
@@ -47,10 +48,10 @@ 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";
|
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy, webdav-push";
|
||||||
|
|
||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
@@ -126,6 +127,13 @@ impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarResourceSer
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn post() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
|
Some(|state, req| {
|
||||||
|
let mut service = Handler::with_state(route_post::<C, S>, state);
|
||||||
|
Box::pin(Service::call(&mut service, req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
|
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
|
||||||
{
|
{
|
||||||
Some(|state, req| {
|
Some(|state, req| {
|
||||||
|
|||||||
@@ -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,7 +21,7 @@ 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);
|
||||||
@@ -33,7 +33,7 @@ pub async fn get_event<C: CalendarStore>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let event = cal_store
|
let event = cal_store
|
||||||
.get_object(&principal, &calendar_id, &object_id)
|
.get_object(&principal, &calendar_id, &object_id, false)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut resp = Response::builder().status(StatusCode::OK);
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
@@ -51,7 +51,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,
|
||||||
|
|||||||
@@ -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,9 +25,11 @@ 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;
|
||||||
|
|
||||||
const IS_COLLECTION: bool = false;
|
fn is_collection(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn get_resourcetype(&self) -> Resourcetype {
|
fn get_resourcetype(&self) -> Resourcetype {
|
||||||
Resourcetype(&[])
|
Resourcetype(&[])
|
||||||
@@ -36,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 {
|
||||||
@@ -79,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),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -61,7 +61,7 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
|||||||
) -> 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)
|
.get_object(principal, calendar_id, object_id, false)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(CalendarObjectResource {
|
Ok(CalendarObjectResource {
|
||||||
object,
|
object,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ pub mod calendar;
|
|||||||
pub mod calendar_object;
|
pub mod calendar_object;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod principal;
|
pub mod principal;
|
||||||
// mod subscription;
|
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
@@ -45,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))),
|
||||||
|
|||||||
@@ -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,26 +27,28 @@ 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;
|
||||||
|
|
||||||
const IS_COLLECTION: bool = true;
|
fn is_collection(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn get_resourcetype(&self) -> Resourcetype {
|
fn get_resourcetype(&self) -> Resourcetype {
|
||||||
Resourcetype(&[
|
Resourcetype(&[
|
||||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
||||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
|
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
|
||||||
// https://github.com/apple/ccs-calendarserver/blob/13c706b985fb728b9aab42dc0fef85aae21921c3/doc/Extensions/caldav-proxy.txt
|
// https://github.com/apple/ccs-calendarserver/blob/13c706b985fb728b9aab42dc0fef85aae21921c3/doc/Extensions/caldav-proxy.txt
|
||||||
ResourcetypeInner(
|
// ResourcetypeInner(
|
||||||
Some(rustical_dav::namespace::NS_CALENDARSERVER),
|
// Some(rustical_dav::namespace::NS_CALENDARSERVER),
|
||||||
"calendar-proxy-write",
|
// "calendar-proxy-write",
|
||||||
),
|
// ),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -111,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),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use actix_web::{
|
|
||||||
HttpResponse,
|
|
||||||
web::{self, Data, Path},
|
|
||||||
};
|
|
||||||
use rustical_dav::xml::multistatus::PropstatElement;
|
|
||||||
use rustical_store::SubscriptionStore;
|
|
||||||
use rustical_xml::{XmlRootTag, XmlSerialize};
|
|
||||||
|
|
||||||
use crate::calendar::resource::CalendarProp;
|
|
||||||
|
|
||||||
async fn handle_delete<S: SubscriptionStore>(
|
|
||||||
store: Data<S>,
|
|
||||||
path: Path<String>,
|
|
||||||
) -> Result<HttpResponse, rustical_store::Error> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
store.delete_subscription(&id).await?;
|
|
||||||
Ok(HttpResponse::NoContent().body("Unregistered"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscription_resource<S: SubscriptionStore>(sub_store: Arc<S>) -> actix_web::Resource {
|
|
||||||
web::resource("/subscription/{id}")
|
|
||||||
.app_data(Data::from(sub_store))
|
|
||||||
.name("subscription")
|
|
||||||
.delete(handle_delete::<S>)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(XmlSerialize, XmlRootTag)]
|
|
||||||
#[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
pub struct PushMessage {
|
|
||||||
propstat: PropstatElement<CalendarProp>,
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,9 +30,11 @@ 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;
|
||||||
|
|
||||||
const IS_COLLECTION: bool = false;
|
fn is_collection(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn get_resourcetype(&self) -> Resourcetype {
|
fn get_resourcetype(&self) -> Resourcetype {
|
||||||
Resourcetype(&[])
|
Resourcetype(&[])
|
||||||
@@ -41,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 {
|
||||||
@@ -76,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),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pub mod mkcol;
|
|
||||||
// pub mod post;
|
|
||||||
pub mod get;
|
pub mod get;
|
||||||
|
pub mod mkcol;
|
||||||
|
pub mod post;
|
||||||
pub mod put;
|
pub mod put;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
@@ -1,33 +1,40 @@
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::addressbook::resource::AddressbookResourceService;
|
use crate::addressbook::AddressbookResourceService;
|
||||||
use actix_web::http::header;
|
use crate::addressbook::resource::AddressbookResource;
|
||||||
use actix_web::web::{Data, Path};
|
use axum::extract::{Path, State};
|
||||||
use actix_web::{HttpRequest, HttpResponse};
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use http::{HeaderMap, HeaderValue, StatusCode, header};
|
||||||
|
use rustical_dav::privileges::UserPrivilege;
|
||||||
|
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;
|
||||||
use tracing_actix_web::RootSpan;
|
|
||||||
|
|
||||||
#[instrument(parent = root_span.id(), skip(resource_service, root_span, req))]
|
#[instrument(skip(resource_service))]
|
||||||
pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
|
pub async fn route_post<AS: AddressbookStore, S: SubscriptionStore>(
|
||||||
path: Path<(String, String)>,
|
Path((principal, addr_id)): Path<(String, String)>,
|
||||||
|
user: Principal,
|
||||||
|
State(resource_service): State<AddressbookResourceService<AS, S>>,
|
||||||
body: String,
|
body: String,
|
||||||
user: User,
|
) -> Result<Response, Error> {
|
||||||
resource_service: Data<AddressbookResourceService<A, S>>,
|
|
||||||
root_span: RootSpan,
|
|
||||||
req: HttpRequest,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let (principal, addressbook_id) = path.into_inner();
|
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let addressbook = resource_service
|
let addressbook = resource_service
|
||||||
.addr_store
|
.addr_store
|
||||||
.get_addressbook(&principal, &addressbook_id, false)
|
.get_addressbook(&principal, &addr_id, false)
|
||||||
.await?;
|
.await?;
|
||||||
|
let addressbook_resource = AddressbookResource(addressbook);
|
||||||
|
if !addressbook_resource
|
||||||
|
.get_user_privileges(&user)?
|
||||||
|
.has(&UserPrivilege::Read)
|
||||||
|
{
|
||||||
|
return Err(Error::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
let request = PushRegister::parse_str(&body)?;
|
let request = PushRegister::parse_str(&body)?;
|
||||||
let sub_id = uuid::Uuid::new_v4().to_string();
|
let sub_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
@@ -44,7 +51,7 @@ pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
|
|||||||
.web_push_subscription
|
.web_push_subscription
|
||||||
.push_resource
|
.push_resource
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
topic: addressbook.push_topic,
|
topic: addressbook_resource.0.push_topic,
|
||||||
expiration: expires.naive_local(),
|
expiration: expires.naive_local(),
|
||||||
public_key: request
|
public_key: request
|
||||||
.subscription
|
.subscription
|
||||||
@@ -63,13 +70,17 @@ pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
|
|||||||
.upsert_subscription(subscription)
|
.upsert_subscription(subscription)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let location = req
|
// TODO: make nicer
|
||||||
.resource_map()
|
let location = format!("/push_subscription/{sub_id}");
|
||||||
.url_for(&req, "subscription", &[sub_id])
|
Ok((
|
||||||
.unwrap();
|
StatusCode::CREATED,
|
||||||
|
HeaderMap::from_iter([
|
||||||
Ok(HttpResponse::Created()
|
(header::LOCATION, HeaderValue::from_str(&location).unwrap()),
|
||||||
.append_header((header::LOCATION, location.to_string()))
|
(
|
||||||
.append_header((header::EXPIRES, expires.to_rfc2822()))
|
header::EXPIRES,
|
||||||
.finish())
|
HeaderValue::from_str(&expires.to_rfc2822()).unwrap(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>>,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,9 +36,11 @@ 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;
|
||||||
|
|
||||||
const IS_COLLECTION: bool = true;
|
fn is_collection(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn get_resourcetype(&self) -> Resourcetype {
|
fn get_resourcetype(&self) -> Resourcetype {
|
||||||
Resourcetype(&[
|
Resourcetype(&[
|
||||||
@@ -50,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 {
|
||||||
@@ -136,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),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use super::methods::report::route_report_addressbook;
|
|||||||
use crate::address_object::AddressObjectResourceService;
|
use crate::address_object::AddressObjectResourceService;
|
||||||
use crate::address_object::resource::AddressObjectResource;
|
use crate::address_object::resource::AddressObjectResource;
|
||||||
use crate::addressbook::methods::get::route_get;
|
use crate::addressbook::methods::get::route_get;
|
||||||
|
use crate::addressbook::methods::post::route_post;
|
||||||
use crate::addressbook::methods::put::route_put;
|
use crate::addressbook::methods::put::route_put;
|
||||||
use crate::addressbook::resource::AddressbookResource;
|
use crate::addressbook::resource::AddressbookResource;
|
||||||
use crate::{CardDavPrincipalUri, Error};
|
use crate::{CardDavPrincipalUri, Error};
|
||||||
@@ -13,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;
|
||||||
@@ -50,10 +51,10 @@ 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";
|
const DAV_HEADER: &str = "1, 3, access-control, addressbook, webdav-push";
|
||||||
|
|
||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
@@ -130,6 +131,13 @@ impl<AS: AddressbookStore, S: SubscriptionStore> AxumMethods for AddressbookReso
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn post() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
|
Some(|state, req| {
|
||||||
|
let mut service = Handler::with_state(route_post::<AS, S>, state);
|
||||||
|
Box::pin(Service::call(&mut service, req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
Some(|state, req| {
|
Some(|state, req| {
|
||||||
let mut service = Handler::with_state(route_put::<AS, S>, state);
|
let mut service = Handler::with_state(route_put::<AS, S>, state);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,9 +27,11 @@ 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;
|
||||||
|
|
||||||
const IS_COLLECTION: bool = true;
|
fn is_collection(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn get_resourcetype(&self) -> Resourcetype {
|
fn get_resourcetype(&self) -> Resourcetype {
|
||||||
Resourcetype(&[
|
Resourcetype(&[
|
||||||
@@ -41,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));
|
||||||
@@ -97,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),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -26,3 +26,5 @@ tokio.workspace = true
|
|||||||
http.workspace = true
|
http.workspace = true
|
||||||
headers.workspace = true
|
headers.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
|
matchit.workspace = true
|
||||||
|
matchit-serde.workspace = true
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("Precondition Failed")]
|
#[error("Precondition Failed")]
|
||||||
PreconditionFailed,
|
PreconditionFailed,
|
||||||
|
|
||||||
|
#[error("Forbidden")]
|
||||||
|
Forbidden,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
@@ -49,6 +52,7 @@ impl Error {
|
|||||||
Error::PropReadOnly => StatusCode::CONFLICT,
|
Error::PropReadOnly => StatusCode::CONFLICT,
|
||||||
Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
|
Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
|
||||||
Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::Forbidden => StatusCode::FORBIDDEN,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,54 @@
|
|||||||
use axum::{
|
|
||||||
extract::{Path, State},
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use http::StatusCode;
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
header::{Depth, Overwrite},
|
header::{Depth, Overwrite},
|
||||||
resource::ResourceService,
|
resource::ResourceService,
|
||||||
};
|
};
|
||||||
|
use axum::{
|
||||||
|
extract::{MatchedPath, Path, State},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use http::{HeaderMap, StatusCode, Uri};
|
||||||
|
use matchit_serde::ParamsDeserializer;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
#[instrument(skip(_path, _resource_service,))]
|
#[instrument(skip(path, resource_service,))]
|
||||||
pub(crate) async fn axum_route_copy<R: ResourceService>(
|
pub(crate) async fn axum_route_copy<R: ResourceService>(
|
||||||
Path(_path): Path<R::PathComponents>,
|
Path(path): Path<R::PathComponents>,
|
||||||
State(_resource_service): State<R>,
|
State(resource_service): State<R>,
|
||||||
depth: Option<Depth>,
|
depth: Option<Depth>,
|
||||||
principal: R::Principal,
|
principal: R::Principal,
|
||||||
overwrite: Overwrite,
|
overwrite: Overwrite,
|
||||||
|
matched_path: MatchedPath,
|
||||||
|
header_map: HeaderMap,
|
||||||
) -> Result<Response, R::Error> {
|
) -> Result<Response, R::Error> {
|
||||||
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but
|
let destination = header_map
|
||||||
// can return a 403 error
|
.get("Destination")
|
||||||
let _depth = depth.unwrap_or(Depth::Infinity);
|
.ok_or(crate::Error::Forbidden)?
|
||||||
Ok(StatusCode::FORBIDDEN.into_response())
|
.to_str()
|
||||||
|
.map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
let destination_uri: Uri = destination.parse().map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
// TODO: Check that host also matches
|
||||||
|
let destination = destination_uri.path();
|
||||||
|
|
||||||
|
let mut router = matchit::Router::new();
|
||||||
|
router.insert(matched_path.as_str(), ()).unwrap();
|
||||||
|
if let Ok(matchit::Match { params, .. }) = router.at(destination) {
|
||||||
|
let params =
|
||||||
|
matchit_serde::Params::try_from(¶ms).map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
let dest_path = R::PathComponents::deserialize(&ParamsDeserializer::new(params))
|
||||||
|
.map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
|
||||||
|
if resource_service
|
||||||
|
.copy_resource(&path, &dest_path, &principal, overwrite.is_true())
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
// Overwritten
|
||||||
|
Ok(StatusCode::NO_CONTENT.into_response())
|
||||||
|
} else {
|
||||||
|
// Not overwritten
|
||||||
|
Ok(StatusCode::CREATED.into_response())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(StatusCode::FORBIDDEN.into_response())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,54 @@
|
|||||||
use axum::{
|
|
||||||
extract::{Path, State},
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use http::StatusCode;
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
header::{Depth, Overwrite},
|
header::{Depth, Overwrite},
|
||||||
resource::ResourceService,
|
resource::ResourceService,
|
||||||
};
|
};
|
||||||
|
use axum::{
|
||||||
|
extract::{MatchedPath, Path, State},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use http::{HeaderMap, StatusCode, Uri};
|
||||||
|
use matchit_serde::ParamsDeserializer;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
#[instrument(skip(_path, _resource_service,))]
|
#[instrument(skip(path, resource_service,))]
|
||||||
pub(crate) async fn axum_route_move<R: ResourceService>(
|
pub(crate) async fn axum_route_move<R: ResourceService>(
|
||||||
Path(_path): Path<R::PathComponents>,
|
Path(path): Path<R::PathComponents>,
|
||||||
State(_resource_service): State<R>,
|
State(resource_service): State<R>,
|
||||||
depth: Option<Depth>,
|
depth: Option<Depth>,
|
||||||
principal: R::Principal,
|
principal: R::Principal,
|
||||||
overwrite: Overwrite,
|
overwrite: Overwrite,
|
||||||
|
matched_path: MatchedPath,
|
||||||
|
header_map: HeaderMap,
|
||||||
) -> Result<Response, R::Error> {
|
) -> Result<Response, R::Error> {
|
||||||
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but
|
let destination = header_map
|
||||||
// can return a 403 error
|
.get("Destination")
|
||||||
let _depth = depth.unwrap_or(Depth::Infinity);
|
.ok_or(crate::Error::Forbidden)?
|
||||||
Ok(StatusCode::FORBIDDEN.into_response())
|
.to_str()
|
||||||
|
.map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
let destination_uri: Uri = destination.parse().map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
// TODO: Check that host also matches
|
||||||
|
let destination = destination_uri.path();
|
||||||
|
|
||||||
|
let mut router = matchit::Router::new();
|
||||||
|
router.insert(matched_path.as_str(), ()).unwrap();
|
||||||
|
if let Ok(matchit::Match { params, .. }) = router.at(destination) {
|
||||||
|
let params =
|
||||||
|
matchit_serde::Params::try_from(¶ms).map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
let dest_path = R::PathComponents::deserialize(&ParamsDeserializer::new(params))
|
||||||
|
.map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
|
||||||
|
if resource_service
|
||||||
|
.copy_resource(&path, &dest_path, &principal, overwrite.is_true())
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
// Overwritten
|
||||||
|
Ok(StatusCode::NO_CONTENT.into_response())
|
||||||
|
} else {
|
||||||
|
// Not overwritten
|
||||||
|
Ok(StatusCode::CREATED.into_response())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(StatusCode::FORBIDDEN.into_response())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ mod methods;
|
|||||||
mod principal_uri;
|
mod principal_uri;
|
||||||
mod resource_service;
|
mod resource_service;
|
||||||
|
|
||||||
pub use axum_methods::AxumMethods;
|
pub use axum_methods::{AxumMethods, MethodFunction};
|
||||||
pub use axum_service::AxumService;
|
pub use axum_service::AxumService;
|
||||||
pub use principal_uri::PrincipalUri;
|
pub use principal_uri::PrincipalUri;
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ pub trait Resource: Clone + Send + 'static {
|
|||||||
type Error: From<crate::Error>;
|
type Error: From<crate::Error>;
|
||||||
type Principal: Principal;
|
type Principal: Principal;
|
||||||
|
|
||||||
const IS_COLLECTION: bool;
|
fn is_collection(&self) -> bool;
|
||||||
|
|
||||||
fn get_resourcetype(&self) -> Resourcetype;
|
fn get_resourcetype(&self) -> Resourcetype;
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ pub trait Resource: Clone + Send + 'static {
|
|||||||
) -> Result<ResponseElement<Self::Prop>, Self::Error> {
|
) -> Result<ResponseElement<Self::Prop>, Self::Error> {
|
||||||
// Collections have a trailing slash
|
// Collections have a trailing slash
|
||||||
let mut path = path.to_string();
|
let mut path = path.to_string();
|
||||||
if Self::IS_COLLECTION && !path.ends_with('/') {
|
if self.is_collection() && !path.ends_with('/') {
|
||||||
path.push('/');
|
path.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ use serde::Deserialize;
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
|
pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
|
||||||
type PathComponents: for<'de> Deserialize<'de> + Sized + Send + Sync + Clone + 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String)
|
type PathComponents: std::fmt::Debug
|
||||||
|
+ for<'de> Deserialize<'de>
|
||||||
|
+ Sized
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ Clone
|
||||||
|
+ 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String)
|
||||||
type MemberType: Resource<Error = Self::Error, Principal = Self::Principal>
|
type MemberType: Resource<Error = Self::Error, Principal = Self::Principal>
|
||||||
+ super::ResourceName;
|
+ super::ResourceName;
|
||||||
type Resource: Resource<Error = Self::Error, Principal = Self::Principal>;
|
type Resource: Resource<Error = Self::Error, Principal = Self::Principal>;
|
||||||
@@ -47,6 +53,28 @@ pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
|
|||||||
Err(crate::Error::Unauthorized.into())
|
Err(crate::Error::Unauthorized.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns whether an existing resource was overwritten
|
||||||
|
async fn copy_resource(
|
||||||
|
&self,
|
||||||
|
_path: &Self::PathComponents,
|
||||||
|
_destination: &Self::PathComponents,
|
||||||
|
_user: &Self::Principal,
|
||||||
|
_overwrite: bool,
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
Err(crate::Error::Forbidden.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns whether an existing resource was overwritten
|
||||||
|
async fn move_resource(
|
||||||
|
&self,
|
||||||
|
_path: &Self::PathComponents,
|
||||||
|
_destination: &Self::PathComponents,
|
||||||
|
_user: &Self::Principal,
|
||||||
|
_overwrite: bool,
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
Err(crate::Error::Forbidden.into())
|
||||||
|
}
|
||||||
|
|
||||||
fn axum_service(self) -> AxumService<Self>
|
fn axum_service(self) -> AxumService<Self>
|
||||||
where
|
where
|
||||||
Self: AxumMethods,
|
Self: AxumMethods,
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ impl<PR: Resource, P: Principal> Resource for RootResource<PR, P> {
|
|||||||
type Error = PR::Error;
|
type Error = PR::Error;
|
||||||
type Principal = P;
|
type Principal = P;
|
||||||
|
|
||||||
const IS_COLLECTION: bool = true;
|
fn is_collection(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn get_resourcetype(&self) -> Resourcetype {
|
fn get_resourcetype(&self) -> Resourcetype {
|
||||||
Resourcetype(&[ResourcetypeInner(
|
Resourcetype(&[ResourcetypeInner(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::xml::TagList;
|
use crate::xml::TagList;
|
||||||
|
use headers::{CacheControl, ContentType, HeaderMapExt};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use quick_xml::name::Namespace;
|
use quick_xml::name::Namespace;
|
||||||
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||||
@@ -109,7 +110,6 @@ impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
|
|||||||
{
|
{
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use http::header;
|
|
||||||
|
|
||||||
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
||||||
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
|
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
|
||||||
@@ -118,9 +118,9 @@ impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
|
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
|
||||||
resp.headers_mut()
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
.unwrap()
|
hdrs.typed_insert(ContentType::xml());
|
||||||
.insert(header::CONTENT_TYPE, "application/xml".try_into().unwrap());
|
hdrs.typed_insert(CacheControl::new().with_no_cache());
|
||||||
resp.body(Body::from(output)).unwrap()
|
resp.body(Body::from(output)).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,3 +23,8 @@ tokio.workspace = true
|
|||||||
rustical_dav.workspace = true
|
rustical_dav.workspace = true
|
||||||
rustical_store.workspace = true
|
rustical_store.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
ece.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
openssl.workspace = true
|
||||||
|
|||||||
23
crates/dav_push/src/endpoints.rs
Normal file
23
crates/dav_push/src/endpoints.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
routing::delete,
|
||||||
|
};
|
||||||
|
use http::StatusCode;
|
||||||
|
use rustical_store::SubscriptionStore;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
async fn handle_delete<S: SubscriptionStore>(
|
||||||
|
State(store): State<Arc<S>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<Response, rustical_store::Error> {
|
||||||
|
store.delete_subscription(&id).await?;
|
||||||
|
Ok((StatusCode::NO_CONTENT, "Unregistered").into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscription_service<S: SubscriptionStore>(sub_store: Arc<S>) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/push_subscription/{id}", delete(handle_delete::<S>))
|
||||||
|
.with_state(sub_store)
|
||||||
|
}
|
||||||
@@ -1,14 +1,41 @@
|
|||||||
mod extension;
|
mod extension;
|
||||||
pub mod notifier;
|
|
||||||
mod prop;
|
mod prop;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
|
use base64::Engine;
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
pub use extension::*;
|
pub use extension::*;
|
||||||
|
use http::{HeaderValue, Method, header};
|
||||||
pub use prop::*;
|
pub use prop::*;
|
||||||
use rustical_store::{CollectionOperation, SubscriptionStore};
|
use reqwest::{Body, Url};
|
||||||
use std::sync::Arc;
|
use rustical_store::{
|
||||||
|
CollectionOperation, CollectionOperationInfo, Subscription, SubscriptionStore,
|
||||||
|
};
|
||||||
|
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||||
|
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tracing::error;
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
mod endpoints;
|
||||||
|
pub use endpoints::subscription_service;
|
||||||
|
|
||||||
|
#[derive(XmlSerialize, Debug)]
|
||||||
|
pub struct ContentUpdate {
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
|
sync_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlSerialize, XmlRootTag, Debug)]
|
||||||
|
#[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
||||||
|
#[xml(ns_prefix(
|
||||||
|
rustical_dav::namespace::NS_DAVPUSH = b"",
|
||||||
|
rustical_dav::namespace::NS_DAV = b"D",
|
||||||
|
))]
|
||||||
|
struct PushMessage {
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
||||||
|
topic: String,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
||||||
|
content_update: Option<ContentUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Constructor)]
|
#[derive(Debug, Constructor)]
|
||||||
pub struct DavPushController<S: SubscriptionStore> {
|
pub struct DavPushController<S: SubscriptionStore> {
|
||||||
@@ -18,14 +45,176 @@ pub struct DavPushController<S: SubscriptionStore> {
|
|||||||
|
|
||||||
impl<S: SubscriptionStore> DavPushController<S> {
|
impl<S: SubscriptionStore> DavPushController<S> {
|
||||||
pub async fn notifier(&self, mut recv: Receiver<CollectionOperation>) {
|
pub async fn notifier(&self, mut recv: Receiver<CollectionOperation>) {
|
||||||
while let Some(message) = recv.recv().await {
|
loop {
|
||||||
let subscribers = match self.sub_store.get_subscriptions(&message.topic).await {
|
// Make sure we don't flood the subscribers
|
||||||
Ok(subs) => subs,
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||||
Err(err) => {
|
let mut messages = vec![];
|
||||||
error!("{err}");
|
recv.recv_many(&mut messages, 100).await;
|
||||||
continue;
|
|
||||||
|
// Right now we just have to show the latest content update by topic
|
||||||
|
// This might become more complicated in the future depending on what kind of updates
|
||||||
|
// we add
|
||||||
|
let mut latest_messages = HashMap::new();
|
||||||
|
for message in messages {
|
||||||
|
if matches!(message.data, CollectionOperationInfo::Content { .. }) {
|
||||||
|
latest_messages.insert(message.topic.to_string(), message);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
let messages = latest_messages.into_values();
|
||||||
|
|
||||||
|
for message in messages {
|
||||||
|
self.send_message(message).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_message(&self, message: CollectionOperation) {
|
||||||
|
let subscriptions = match self.sub_store.get_subscriptions(&message.topic).await {
|
||||||
|
Ok(subs) => subs,
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if subscriptions.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(message.data, CollectionOperationInfo::Delete) {
|
||||||
|
// Collection has been deleted, but we cannot handle that
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_update = if let CollectionOperationInfo::Content { sync_token } = message.data {
|
||||||
|
Some(ContentUpdate {
|
||||||
|
sync_token: Some(sync_token),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let push_message = PushMessage {
|
||||||
|
topic: message.topic,
|
||||||
|
content_update,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
||||||
|
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
|
||||||
|
if let Err(err) = push_message.serialize_root(&mut writer) {
|
||||||
|
error!("Could not serialize push message: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let payload = String::from_utf8(output).unwrap();
|
||||||
|
|
||||||
|
for subsciption in subscriptions {
|
||||||
|
if let Some(allowed_push_servers) = &self.allowed_push_servers {
|
||||||
|
if let Ok(url) = Url::parse(&subsciption.push_resource) {
|
||||||
|
let origin = url.origin().unicode_serialization();
|
||||||
|
if !allowed_push_servers.contains(&origin) {
|
||||||
|
warn!(
|
||||||
|
"Deleting subscription {} on topic {} because the endpoint is not in the list of allowed push servers",
|
||||||
|
subsciption.id, subsciption.topic
|
||||||
|
);
|
||||||
|
self.try_delete_subscription(&subsciption.id).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Deleting subscription {} on topic {} because of invalid URL",
|
||||||
|
subsciption.id, subsciption.topic
|
||||||
|
);
|
||||||
|
self.try_delete_subscription(&subsciption.id).await;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = self.send_payload(&payload, &subsciption).await {
|
||||||
|
error!("An error occured sending out a push notification: {err}");
|
||||||
|
if err.is_permament_error() {
|
||||||
|
warn!(
|
||||||
|
"Deleting subscription {} on topic {}",
|
||||||
|
subsciption.id, subsciption.topic
|
||||||
|
);
|
||||||
|
self.try_delete_subscription(&subsciption.id).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_delete_subscription(&self, sub_id: &str) {
|
||||||
|
if let Err(err) = self.sub_store.delete_subscription(sub_id).await {
|
||||||
|
error!("Error deleting subsciption: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_payload(
|
||||||
|
&self,
|
||||||
|
payload: &str,
|
||||||
|
subsciption: &Subscription,
|
||||||
|
) -> Result<(), NotifierError> {
|
||||||
|
if subsciption.public_key_type != "p256dh" {
|
||||||
|
return Err(NotifierError::InvalidPublicKeyType(
|
||||||
|
subsciption.public_key_type.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let endpoint = subsciption.push_resource.parse().map_err(|_| {
|
||||||
|
NotifierError::InvalidEndpointUrl(subsciption.push_resource.to_string())
|
||||||
|
})?;
|
||||||
|
let ua_public = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||||
|
.decode(&subsciption.public_key)
|
||||||
|
.map_err(|_| NotifierError::InvalidKeyEncoding)?;
|
||||||
|
let auth_secret = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||||
|
.decode(&subsciption.auth_secret)
|
||||||
|
.map_err(|_| NotifierError::InvalidKeyEncoding)?;
|
||||||
|
|
||||||
|
let client = reqwest::ClientBuilder::new()
|
||||||
|
.build()
|
||||||
|
.map_err(NotifierError::from)?;
|
||||||
|
|
||||||
|
let payload = ece::encrypt(&ua_public, &auth_secret, payload.as_bytes())?;
|
||||||
|
|
||||||
|
let mut request = reqwest::Request::new(Method::POST, endpoint);
|
||||||
|
*request.body_mut() = Some(Body::from(payload));
|
||||||
|
let hdrs = request.headers_mut();
|
||||||
|
hdrs.insert(
|
||||||
|
header::CONTENT_ENCODING,
|
||||||
|
HeaderValue::from_static("aes128gcm"),
|
||||||
|
);
|
||||||
|
hdrs.insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static("application/octet-stream"),
|
||||||
|
);
|
||||||
|
client.execute(request).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum NotifierError {
|
||||||
|
#[error("Invalid public key type: {0}")]
|
||||||
|
InvalidPublicKeyType(String),
|
||||||
|
#[error("Invalid endpoint URL: {0}")]
|
||||||
|
InvalidEndpointUrl(String),
|
||||||
|
#[error("Invalid key encoding")]
|
||||||
|
InvalidKeyEncoding,
|
||||||
|
#[error(transparent)]
|
||||||
|
EceError(#[from] ece::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
ReqwestError(#[from] reqwest::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotifierError {
|
||||||
|
// Decide whether the error should cause the subscription to be removed
|
||||||
|
pub fn is_permament_error(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::InvalidPublicKeyType(_)
|
||||||
|
| Self::InvalidEndpointUrl(_)
|
||||||
|
| Self::InvalidKeyEncoding => true,
|
||||||
|
Self::EceError(err) => matches!(
|
||||||
|
err,
|
||||||
|
ece::Error::InvalidAuthSecret | ece::Error::InvalidKeyLength
|
||||||
|
),
|
||||||
|
Self::ReqwestError(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
use http::StatusCode;
|
|
||||||
use reqwest::{
|
|
||||||
Method, Request,
|
|
||||||
header::{self, HeaderName, HeaderValue},
|
|
||||||
};
|
|
||||||
use rustical_dav::xml::multistatus::PropstatElement;
|
|
||||||
use rustical_store::{CollectionOperation, CollectionOperationType, SubscriptionStore};
|
|
||||||
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
|
||||||
use std::{str::FromStr, sync::Arc};
|
|
||||||
use tokio::sync::mpsc::Receiver;
|
|
||||||
use tracing::{error, info, warn};
|
|
||||||
// use web_push::{SubscriptionInfo, WebPushMessage, WebPushMessageBuilder};
|
|
||||||
|
|
||||||
#[derive(XmlSerialize, Debug)]
|
|
||||||
struct PushMessageProp {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
|
||||||
topic: String,
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
|
||||||
sync_token: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(XmlSerialize, XmlRootTag, Debug)]
|
|
||||||
#[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
#[xml(ns_prefix(
|
|
||||||
rustical_dav::namespace::NS_DAVPUSH = b"",
|
|
||||||
rustical_dav::namespace::NS_DAV = b"D",
|
|
||||||
))]
|
|
||||||
struct PushMessage {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
|
||||||
propstat: PropstatElement<PushMessageProp>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub fn build_request(message: WebPushMessage) -> Request {
|
|
||||||
// // A little janky :)
|
|
||||||
// let url = reqwest::Url::from_str(&message.endpoint.to_string()).unwrap();
|
|
||||||
// let mut builder = Request::new(Method::POST, url);
|
|
||||||
//
|
|
||||||
// if let Some(topic) = message.topic {
|
|
||||||
// builder
|
|
||||||
// .headers_mut()
|
|
||||||
// .insert("Topic", HeaderValue::from_str(topic.as_str()).unwrap());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if let Some(payload) = message.payload {
|
|
||||||
// builder.headers_mut().insert(
|
|
||||||
// header::CONTENT_ENCODING,
|
|
||||||
// HeaderValue::from_static(payload.content_encoding.to_str()),
|
|
||||||
// );
|
|
||||||
// builder.headers_mut().insert(
|
|
||||||
// header::CONTENT_TYPE,
|
|
||||||
// HeaderValue::from_static("application/octet-stream"),
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// for (k, v) in payload.crypto_headers.into_iter() {
|
|
||||||
// let v: &str = v.as_ref();
|
|
||||||
// builder.headers_mut().insert(
|
|
||||||
// HeaderName::from_static(k),
|
|
||||||
// HeaderValue::from_str(&v).unwrap(),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// *builder.body_mut() = Some(reqwest::Body::from(payload.content));
|
|
||||||
// }
|
|
||||||
// builder
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub async fn push_notifier(
|
|
||||||
allowed_push_servers: Option<Vec<String>>,
|
|
||||||
mut recv: Receiver<CollectionOperation>,
|
|
||||||
sub_store: Arc<impl SubscriptionStore>,
|
|
||||||
) {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
|
|
||||||
while let Some(message) = recv.recv().await {
|
|
||||||
let subscribers = match sub_store.get_subscriptions(&message.topic).await {
|
|
||||||
Ok(subs) => subs,
|
|
||||||
Err(err) => {
|
|
||||||
error!("{err}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let status = match message.r#type {
|
|
||||||
CollectionOperationType::Object => StatusCode::OK,
|
|
||||||
CollectionOperationType::Delete => StatusCode::NOT_FOUND,
|
|
||||||
};
|
|
||||||
|
|
||||||
let push_message = PushMessage {
|
|
||||||
propstat: PropstatElement {
|
|
||||||
prop: PushMessageProp {
|
|
||||||
topic: message.topic,
|
|
||||||
sync_token: message.sync_token,
|
|
||||||
},
|
|
||||||
status,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
|
||||||
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
|
|
||||||
if let Err(err) = push_message.serialize_root(&mut writer) {
|
|
||||||
error!("Could not serialize push message: {}", err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let payload = String::from_utf8(output).unwrap();
|
|
||||||
// for subscriber in subscribers {
|
|
||||||
// let push_resource = subscriber.push_resource;
|
|
||||||
//
|
|
||||||
// let sub_info = SubscriptionInfo {
|
|
||||||
// endpoint: push_resource.to_owned(),
|
|
||||||
// keys: web_push::SubscriptionKeys {
|
|
||||||
// p256dh: subscriber.public_key,
|
|
||||||
// auth: subscriber.auth_secret,
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// let mut builder = WebPushMessageBuilder::new(&sub_info);
|
|
||||||
// builder.set_payload(web_push::ContentEncoding::Aes128Gcm, payload.as_bytes());
|
|
||||||
// let push_message = builder.build().unwrap();
|
|
||||||
// let request = build_request(push_message);
|
|
||||||
//
|
|
||||||
// let allowed = if let Some(allowed_push_servers) = &allowed_push_servers {
|
|
||||||
// if let Ok(resource_url) = reqwest::Url::parse(&push_resource) {
|
|
||||||
// let origin = resource_url.origin().ascii_serialization();
|
|
||||||
// allowed_push_servers
|
|
||||||
// .iter()
|
|
||||||
// .any(|allowed_push_server| allowed_push_server == &origin)
|
|
||||||
// } else {
|
|
||||||
// warn!("Invalid push url: {push_resource}");
|
|
||||||
// false
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// true
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// if allowed {
|
|
||||||
// info!("Sending a push message to {}: {}", push_resource, payload);
|
|
||||||
// if let Err(err) = client.execute(request).await {
|
|
||||||
// error!("{err}");
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// warn!(
|
|
||||||
// "Not sending a push notification to {} since it's not allowed in dav_push::allowed_push_servers",
|
|
||||||
// push_resource
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2020",
|
"ES2024",
|
||||||
"DOM",
|
"DOM",
|
||||||
"DOM.Iterable"
|
"DOM.Iterable"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -24,12 +25,15 @@ export class CreateAddressbookForm extends LitElement {
|
|||||||
@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.id = e.target.value} />
|
||||||
@@ -46,8 +50,9 @@ 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>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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-calendar-form")
|
@customElement("create-calendar-form")
|
||||||
@@ -30,12 +31,16 @@ 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.id = e.target.value} />
|
||||||
@@ -69,9 +74,10 @@ 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) {
|
||||||
@@ -89,7 +95,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.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>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"module": "nodenext",
|
"module": "nodenext",
|
||||||
"moduleResolution": "nodenext",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2020",
|
"target": "es2024",
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"useDefineForClassFields": false
|
"useDefineForClassFields": false,
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2024"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"lib/**/*.ts"
|
"lib/**/*.ts"
|
||||||
|
|||||||
@@ -1,45 +1,49 @@
|
|||||||
import { i as d, x as m } from "./lit-Dq9MfRDi.mjs";
|
import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
|
||||||
import { n, t as c } from "./property-DwhV4xIV.mjs";
|
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
|
||||||
import { a as u } from "./webdav-Bz4I5vNH.mjs";
|
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
||||||
var h = Object.defineProperty, y = Object.getOwnPropertyDescriptor, r = (e, a, o, s) => {
|
var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, a = (t, s, l, r) => {
|
||||||
for (var t = s > 1 ? void 0 : s ? y(a, o) : a, p = e.length - 1, l; p >= 0; p--)
|
for (var e = r > 1 ? void 0 : r ? f(s, l) : s, n = t.length - 1, p; n >= 0; n--)
|
||||||
(l = e[p]) && (t = (s ? l(a, o, t) : l(t)) || t);
|
(p = t[n]) && (e = (r ? p(s, l, e) : p(e)) || e);
|
||||||
return s && t && h(a, o, t), t;
|
return r && e && y(s, l, e), e;
|
||||||
};
|
};
|
||||||
let i = class extends d {
|
let i = class extends c {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(), this.client = u("/carddav"), this.user = "", this.id = "", this.displayname = "", this.description = "";
|
super(), this.client = b("/carddav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.dialog = d(), this.form = d();
|
||||||
}
|
}
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return m`
|
return u`
|
||||||
<section>
|
<button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
|
||||||
<h3>Create calendar</h3>
|
<dialog ${m(this.dialog)}>
|
||||||
<form @submit=${this.submit}>
|
<h3>Create addressbook</h3>
|
||||||
|
<form @submit=${this.submit} ${m(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" @change=${(e) => this.id = e.target.value} />
|
<input type="text" name="id" @change=${(t) => this.id = t.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=${(t) => this.displayname = t.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=${(t) => this.description = t.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
|
<button type="submit" @click=${(t) => {
|
||||||
|
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
|
||||||
|
}}> Cancel </button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
async submit(e) {
|
async submit(t) {
|
||||||
if (console.log(this.displayname), e.preventDefault(), !this.id) {
|
if (console.log(this.displayname), t.preventDefault(), !this.id) {
|
||||||
alert("Empty id");
|
alert("Empty id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -61,20 +65,20 @@ let i = class extends d {
|
|||||||
}), window.location.reload(), null;
|
}), window.location.reload(), null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
r([
|
a([
|
||||||
n()
|
o()
|
||||||
], i.prototype, "user", 2);
|
], i.prototype, "user", 2);
|
||||||
r([
|
a([
|
||||||
n()
|
o()
|
||||||
], i.prototype, "id", 2);
|
], i.prototype, "id", 2);
|
||||||
r([
|
a([
|
||||||
n()
|
o()
|
||||||
], i.prototype, "displayname", 2);
|
], i.prototype, "displayname", 2);
|
||||||
r([
|
a([
|
||||||
n()
|
o()
|
||||||
], i.prototype, "description", 2);
|
], i.prototype, "description", 2);
|
||||||
i = r([
|
i = a([
|
||||||
c("create-addressbook-form")
|
h("create-addressbook-form")
|
||||||
], i);
|
], i);
|
||||||
export {
|
export {
|
||||||
i as CreateAddressbookForm
|
i as CreateAddressbookForm
|
||||||
|
|||||||
@@ -1,62 +1,66 @@
|
|||||||
import { i as m, x as c } from "./lit-Dq9MfRDi.mjs";
|
import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
|
||||||
import { n as s, t as d } from "./property-DwhV4xIV.mjs";
|
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
|
||||||
import { a as u } from "./webdav-Bz4I5vNH.mjs";
|
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
||||||
var h = Object.defineProperty, b = Object.getOwnPropertyDescriptor, a = (e, t, o, n) => {
|
var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, l, s) => {
|
||||||
for (var i = n > 1 ? void 0 : n ? b(t, o) : t, l = e.length - 1, p; l >= 0; l--)
|
for (var i = s > 1 ? void 0 : s ? $(e, l) : e, n = t.length - 1, p; n >= 0; n--)
|
||||||
(p = e[l]) && (i = (n ? p(t, o, i) : p(i)) || i);
|
(p = t[n]) && (i = (s ? p(e, l, i) : p(i)) || i);
|
||||||
return n && i && h(t, o, i), i;
|
return s && i && y(e, l, i), i;
|
||||||
};
|
};
|
||||||
let r = class extends m {
|
let r = class extends u {
|
||||||
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 = b("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(), this.dialog = d(), this.form = d();
|
||||||
}
|
}
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return c`
|
return c`
|
||||||
<section>
|
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
||||||
|
<dialog ${m(this.dialog)}>
|
||||||
<h3>Create calendar</h3>
|
<h3>Create calendar</h3>
|
||||||
<form @submit=${this.submit}>
|
<form @submit=${this.submit} ${m(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" @change=${(e) => this.id = e.target.value} />
|
<input type="text" name="id" @change=${(t) => this.id = t.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=${(t) => this.displayname = t.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=${(t) => this.description = t.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=${(t) => this.color = t.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=${(t) => this.subscriptionUrl = t.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
${["VEVENT", "VTODO", "VJOURNAL"].map((e) => c`
|
${["VEVENT", "VTODO", "VJOURNAL"].map((t) => c`
|
||||||
<label>
|
<label>
|
||||||
Support ${e}
|
Support ${t}
|
||||||
<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=${t} @change=${(e) => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
`)}
|
`)}
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
</form>
|
<button type="submit" @click=${(t) => {
|
||||||
</section>
|
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
|
||||||
`;
|
}}> Cancel </button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
async submit(e) {
|
async submit(t) {
|
||||||
if (console.log(this.displayname), e.preventDefault(), !this.id) {
|
if (console.log(this.displayname), t.preventDefault(), !this.id) {
|
||||||
alert("Empty id");
|
alert("Empty id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -68,7 +72,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}`, {
|
return await this.client.createDirectory(`/ principal / ${this.user}/${this.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>
|
||||||
@@ -78,7 +82,7 @@ let r = class extends m {
|
|||||||
${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>${this.subscriptionUrl}</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((e) => `<CAL:comp name="${e}" />`).join(`
|
||||||
`)}
|
`)}
|
||||||
</CAL:supported-calendar-component-set>
|
</CAL:supported-calendar-component-set>
|
||||||
</prop>
|
</prop>
|
||||||
@@ -89,28 +93,28 @@ let r = class extends m {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
a([
|
a([
|
||||||
s()
|
o()
|
||||||
], r.prototype, "user", 2);
|
], r.prototype, "user", 2);
|
||||||
a([
|
a([
|
||||||
s()
|
o()
|
||||||
], r.prototype, "id", 2);
|
], r.prototype, "id", 2);
|
||||||
a([
|
a([
|
||||||
s()
|
o()
|
||||||
], r.prototype, "displayname", 2);
|
], r.prototype, "displayname", 2);
|
||||||
a([
|
a([
|
||||||
s()
|
o()
|
||||||
], r.prototype, "description", 2);
|
], r.prototype, "description", 2);
|
||||||
a([
|
a([
|
||||||
s()
|
o()
|
||||||
], r.prototype, "color", 2);
|
], r.prototype, "color", 2);
|
||||||
a([
|
a([
|
||||||
s()
|
o()
|
||||||
], r.prototype, "subscriptionUrl", 2);
|
], r.prototype, "subscriptionUrl", 2);
|
||||||
a([
|
a([
|
||||||
s()
|
o()
|
||||||
], r.prototype, "components", 2);
|
], r.prototype, "components", 2);
|
||||||
r = a([
|
r = a([
|
||||||
d("create-calendar-form")
|
h("create-calendar-form")
|
||||||
], r);
|
], r);
|
||||||
export {
|
export {
|
||||||
r as CreateCalendarForm
|
r as CreateCalendarForm
|
||||||
|
|||||||
@@ -543,6 +543,7 @@ const z = y.litElementPolyfillSupport;
|
|||||||
z == null || z({ LitElement: T });
|
z == null || z({ LitElement: T });
|
||||||
(y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0");
|
(y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0");
|
||||||
export {
|
export {
|
||||||
|
d as E,
|
||||||
et as f,
|
et as f,
|
||||||
T as i,
|
T as i,
|
||||||
j as u,
|
j as u,
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
172
crates/frontend/public/assets/js/ref-DuYNkSJ_.mjs
Normal file
172
crates/frontend/public/assets/js/ref-DuYNkSJ_.mjs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { f, u as _, E as $ } from "./lit-CWlWuEHk.mjs";
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const T = (t) => (e, s) => {
|
||||||
|
s !== void 0 ? s.addInitializer(() => {
|
||||||
|
customElements.define(t, e);
|
||||||
|
}) : customElements.define(t, e);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const A = { attribute: !0, type: String, converter: _, reflect: !1, hasChanged: f }, p = (t = A, e, s) => {
|
||||||
|
const { kind: i, metadata: n } = s;
|
||||||
|
let r = globalThis.litPropertyMetadata.get(n);
|
||||||
|
if (r === void 0 && globalThis.litPropertyMetadata.set(n, r = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), r.set(s.name, t), i === "accessor") {
|
||||||
|
const { name: o } = s;
|
||||||
|
return { set(h) {
|
||||||
|
const l = e.get.call(this);
|
||||||
|
e.set.call(this, h), this.requestUpdate(o, l, t);
|
||||||
|
}, init(h) {
|
||||||
|
return h !== void 0 && this.C(o, void 0, t, h), h;
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
if (i === "setter") {
|
||||||
|
const { name: o } = s;
|
||||||
|
return function(h) {
|
||||||
|
const l = this[o];
|
||||||
|
e.call(this, h), this.requestUpdate(o, l, t);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw Error("Unsupported decorator location: " + i);
|
||||||
|
};
|
||||||
|
function O(t) {
|
||||||
|
return (e, s) => typeof s == "object" ? p(t, e, s) : ((i, n, r) => {
|
||||||
|
const o = n.hasOwnProperty(r);
|
||||||
|
return n.constructor.createProperty(r, i), o ? Object.getOwnPropertyDescriptor(n, r) : void 0;
|
||||||
|
})(t, e, s);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const v = (t) => t.strings === void 0;
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const g = { CHILD: 2 }, C = (t) => (...e) => ({ _$litDirective$: t, values: e });
|
||||||
|
class m {
|
||||||
|
constructor(e) {
|
||||||
|
}
|
||||||
|
get _$AU() {
|
||||||
|
return this._$AM._$AU;
|
||||||
|
}
|
||||||
|
_$AT(e, s, i) {
|
||||||
|
this._$Ct = e, this._$AM = s, this._$Ci = i;
|
||||||
|
}
|
||||||
|
_$AS(e, s) {
|
||||||
|
return this.update(e, s);
|
||||||
|
}
|
||||||
|
update(e, s) {
|
||||||
|
return this.render(...s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const c = (t, e) => {
|
||||||
|
var i;
|
||||||
|
const s = t._$AN;
|
||||||
|
if (s === void 0) return !1;
|
||||||
|
for (const n of s) (i = n._$AO) == null || i.call(n, e, !1), c(n, e);
|
||||||
|
return !0;
|
||||||
|
}, a = (t) => {
|
||||||
|
let e, s;
|
||||||
|
do {
|
||||||
|
if ((e = t._$AM) === void 0) break;
|
||||||
|
s = e._$AN, s.delete(t), t = e;
|
||||||
|
} while ((s == null ? void 0 : s.size) === 0);
|
||||||
|
}, u = (t) => {
|
||||||
|
for (let e; e = t._$AM; t = e) {
|
||||||
|
let s = e._$AN;
|
||||||
|
if (s === void 0) e._$AN = s = /* @__PURE__ */ new Set();
|
||||||
|
else if (s.has(t)) break;
|
||||||
|
s.add(t), M(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function y(t) {
|
||||||
|
this._$AN !== void 0 ? (a(this), this._$AM = t, u(this)) : this._$AM = t;
|
||||||
|
}
|
||||||
|
function G(t, e = !1, s = 0) {
|
||||||
|
const i = this._$AH, n = this._$AN;
|
||||||
|
if (n !== void 0 && n.size !== 0) if (e) if (Array.isArray(i)) for (let r = s; r < i.length; r++) c(i[r], !1), a(i[r]);
|
||||||
|
else i != null && (c(i, !1), a(i));
|
||||||
|
else c(this, t);
|
||||||
|
}
|
||||||
|
const M = (t) => {
|
||||||
|
t.type == g.CHILD && (t._$AP ?? (t._$AP = G), t._$AQ ?? (t._$AQ = y));
|
||||||
|
};
|
||||||
|
class b extends m {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments), this._$AN = void 0;
|
||||||
|
}
|
||||||
|
_$AT(e, s, i) {
|
||||||
|
super._$AT(e, s, i), u(this), this.isConnected = e._$AU;
|
||||||
|
}
|
||||||
|
_$AO(e, s = !0) {
|
||||||
|
var i, n;
|
||||||
|
e !== this.isConnected && (this.isConnected = e, e ? (i = this.reconnected) == null || i.call(this) : (n = this.disconnected) == null || n.call(this)), s && (c(this, e), a(this));
|
||||||
|
}
|
||||||
|
setValue(e) {
|
||||||
|
if (v(this._$Ct)) this._$Ct._$AI(e, this);
|
||||||
|
else {
|
||||||
|
const s = [...this._$Ct._$AH];
|
||||||
|
s[this._$Ci] = e, this._$Ct._$AI(s, this, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disconnected() {
|
||||||
|
}
|
||||||
|
reconnected() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const P = () => new w();
|
||||||
|
class w {
|
||||||
|
}
|
||||||
|
const d = /* @__PURE__ */ new WeakMap(), U = C(class extends b {
|
||||||
|
render(t) {
|
||||||
|
return $;
|
||||||
|
}
|
||||||
|
update(t, [e]) {
|
||||||
|
var i;
|
||||||
|
const s = e !== this.G;
|
||||||
|
return s && this.G !== void 0 && this.rt(void 0), (s || this.lt !== this.ct) && (this.G = e, this.ht = (i = t.options) == null ? void 0 : i.host, this.rt(this.ct = t.element)), $;
|
||||||
|
}
|
||||||
|
rt(t) {
|
||||||
|
if (this.isConnected || (t = void 0), typeof this.G == "function") {
|
||||||
|
const e = this.ht ?? globalThis;
|
||||||
|
let s = d.get(e);
|
||||||
|
s === void 0 && (s = /* @__PURE__ */ new WeakMap(), d.set(e, s)), s.get(this.G) !== void 0 && this.G.call(this.ht, void 0), s.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
|
||||||
|
} else this.G.value = t;
|
||||||
|
}
|
||||||
|
get lt() {
|
||||||
|
var t, e;
|
||||||
|
return typeof this.G == "function" ? (t = d.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (e = this.G) == null ? void 0 : e.value;
|
||||||
|
}
|
||||||
|
disconnected() {
|
||||||
|
this.lt === this.ct && this.rt(void 0);
|
||||||
|
}
|
||||||
|
reconnected() {
|
||||||
|
this.rt(this.ct);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export {
|
||||||
|
O as a,
|
||||||
|
P as e,
|
||||||
|
U as n,
|
||||||
|
T as t
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -51,7 +51,7 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
|
|||||||
pub async fn route_delete_addressbook<AS: AddressbookStore>(
|
pub async fn route_delete_addressbook<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,
|
||||||
) -> 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());
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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,7 +21,7 @@ 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());
|
||||||
@@ -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) {
|
||||||
@@ -51,7 +51,7 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
|
|||||||
pub async fn route_delete_calendar<C: CalendarStore>(
|
pub async fn route_delete_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());
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -58,6 +58,7 @@ pub trait CalendarStore: Send + Sync + 'static {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<CalendarObject, Error>;
|
) -> Result<CalendarObject, Error>;
|
||||||
async fn put_object(
|
async fn put_object(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -80,11 +80,13 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
|
|||||||
use_trashbin: bool,
|
use_trashbin: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
||||||
Err(Error::ReadOnly)
|
|
||||||
} else {
|
|
||||||
self.birthday_store
|
self.birthday_store
|
||||||
.delete_object(principal, cal_id, object_id, use_trashbin)
|
.delete_object(principal, cal_id, object_id, use_trashbin)
|
||||||
.await
|
.await
|
||||||
|
} else {
|
||||||
|
self.cal_store
|
||||||
|
.delete_object(principal, cal_id, object_id, use_trashbin)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,14 +96,15 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<CalendarObject, Error> {
|
) -> Result<CalendarObject, Error> {
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
||||||
self.birthday_store
|
self.birthday_store
|
||||||
.get_object(principal, cal_id, object_id)
|
.get_object(principal, cal_id, object_id, show_deleted)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
self.cal_store
|
self.cal_store
|
||||||
.get_object(principal, cal_id, object_id)
|
.get_object(principal, cal_id, object_id, show_deleted)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,4 +240,3 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,13 +126,14 @@ impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<CalendarObject, Error> {
|
) -> Result<CalendarObject, Error> {
|
||||||
let cal_id = cal_id
|
let cal_id = cal_id
|
||||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
.ok_or(Error::NotFound)?;
|
.ok_or(Error::NotFound)?;
|
||||||
let (addressobject_id, date_type) = object_id.rsplit_once("-").ok_or(Error::NotFound)?;
|
let (addressobject_id, date_type) = object_id.rsplit_once("-").ok_or(Error::NotFound)?;
|
||||||
self.0
|
self.0
|
||||||
.get_object(principal, cal_id, addressobject_id, false)
|
.get_object(principal, cal_id, addressobject_id, show_deleted)
|
||||||
.await?
|
.await?
|
||||||
.get_significant_dates()?
|
.get_significant_dates()?
|
||||||
.remove(date_type)
|
.remove(date_type)
|
||||||
|
|||||||
@@ -22,22 +22,15 @@ pub use addressbook::Addressbook;
|
|||||||
pub use calendar::Calendar;
|
pub use calendar::Calendar;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum CollectionOperationType {
|
pub enum CollectionOperationInfo {
|
||||||
// Sync-Token increased
|
// Sync-Token increased
|
||||||
Object,
|
Content { sync_token: String },
|
||||||
|
// Collection deleted
|
||||||
Delete,
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum CollectionOperationDomain {
|
|
||||||
Calendar,
|
|
||||||
Addressbook,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CollectionOperation {
|
pub struct CollectionOperation {
|
||||||
pub r#type: CollectionOperationType,
|
|
||||||
pub domain: CollectionOperationDomain,
|
|
||||||
pub topic: String,
|
pub topic: String,
|
||||||
pub sync_token: Option<String>,
|
pub data: CollectionOperationInfo,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use async_trait::async_trait;
|
|||||||
use derive_more::derive::Constructor;
|
use derive_more::derive::Constructor;
|
||||||
use rustical_ical::AddressObject;
|
use rustical_ical::AddressObject;
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
Addressbook, AddressbookStore, CollectionOperation, CollectionOperationDomain,
|
Addressbook, AddressbookStore, CollectionOperation, CollectionOperationInfo, Error,
|
||||||
CollectionOperationType, Error, synctoken::format_synctoken,
|
synctoken::format_synctoken,
|
||||||
};
|
};
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
@@ -250,7 +250,7 @@ impl SqliteAddressbookStore {
|
|||||||
) -> Result<AddressObject, rustical_store::Error> {
|
) -> Result<AddressObject, rustical_store::Error> {
|
||||||
Ok(sqlx::query_as!(
|
Ok(sqlx::query_as!(
|
||||||
AddressObjectRow,
|
AddressObjectRow,
|
||||||
"SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) or ?)",
|
"SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||||
principal,
|
principal,
|
||||||
addressbook_id,
|
addressbook_id,
|
||||||
object_id,
|
object_id,
|
||||||
@@ -413,10 +413,8 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
if let Some(addressbook) = addressbook {
|
if let Some(addressbook) = addressbook {
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
r#type: CollectionOperationType::Delete,
|
data: CollectionOperationInfo::Delete,
|
||||||
domain: CollectionOperationDomain::Addressbook,
|
|
||||||
topic: addressbook.push_topic,
|
topic: addressbook.push_topic,
|
||||||
sync_token: None,
|
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted addressbook failed: {err}");
|
error!("Push notification about deleted addressbook failed: {err}");
|
||||||
};
|
};
|
||||||
@@ -485,7 +483,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let synctoken = log_object_operation(
|
let sync_token = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&principal,
|
&principal,
|
||||||
&addressbook_id,
|
&addressbook_id,
|
||||||
@@ -498,13 +496,11 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
r#type: CollectionOperationType::Object,
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
domain: CollectionOperationDomain::Addressbook,
|
|
||||||
topic: self
|
topic: self
|
||||||
.get_addressbook(&principal, &addressbook_id, false)
|
.get_addressbook(&principal, &addressbook_id, false)
|
||||||
.await?
|
.await?
|
||||||
.push_topic,
|
.push_topic,
|
||||||
sync_token: Some(synctoken),
|
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted addressbook failed: {err}");
|
error!("Push notification about deleted addressbook failed: {err}");
|
||||||
};
|
};
|
||||||
@@ -524,7 +520,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
Self::_delete_object(&mut *tx, principal, addressbook_id, object_id, use_trashbin).await?;
|
Self::_delete_object(&mut *tx, principal, addressbook_id, object_id, use_trashbin).await?;
|
||||||
|
|
||||||
let synctoken = log_object_operation(
|
let sync_token = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
principal,
|
principal,
|
||||||
addressbook_id,
|
addressbook_id,
|
||||||
@@ -536,16 +532,15 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
// TODO: Watch for errors here?
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
let _ = self.sender.try_send(CollectionOperation {
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
r#type: CollectionOperationType::Object,
|
|
||||||
domain: CollectionOperationDomain::Addressbook,
|
|
||||||
topic: self
|
topic: self
|
||||||
.get_addressbook(principal, addressbook_id, false)
|
.get_addressbook(principal, addressbook_id, false)
|
||||||
.await?
|
.await?
|
||||||
.push_topic,
|
.push_topic,
|
||||||
sync_token: Some(synctoken),
|
}) {
|
||||||
});
|
error!("Push notification about deleted addressbook failed: {err}");
|
||||||
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,7 +555,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
Self::_restore_object(&mut *tx, principal, addressbook_id, object_id).await?;
|
Self::_restore_object(&mut *tx, principal, addressbook_id, object_id).await?;
|
||||||
|
|
||||||
let synctoken = log_object_operation(
|
let sync_token = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
principal,
|
principal,
|
||||||
addressbook_id,
|
addressbook_id,
|
||||||
@@ -571,16 +566,15 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
// TODO: Watch for errors here?
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
let _ = self.sender.try_send(CollectionOperation {
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
r#type: CollectionOperationType::Object,
|
|
||||||
domain: CollectionOperationDomain::Addressbook,
|
|
||||||
topic: self
|
topic: self
|
||||||
.get_addressbook(principal, addressbook_id, false)
|
.get_addressbook(principal, addressbook_id, false)
|
||||||
.await?
|
.await?
|
||||||
.push_topic,
|
.push_topic,
|
||||||
sync_token: Some(synctoken),
|
}) {
|
||||||
});
|
error!("Push notification about deleted addressbook failed: {err}");
|
||||||
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
|
|||||||
use rustical_store::calendar_store::CalendarQuery;
|
use rustical_store::calendar_store::CalendarQuery;
|
||||||
use rustical_store::synctoken::format_synctoken;
|
use rustical_store::synctoken::format_synctoken;
|
||||||
use rustical_store::{Calendar, CalendarStore, Error};
|
use rustical_store::{Calendar, CalendarStore, Error};
|
||||||
use rustical_store::{CollectionOperation, CollectionOperationType};
|
use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
||||||
use sqlx::types::chrono::NaiveDateTime;
|
use sqlx::types::chrono::NaiveDateTime;
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
@@ -296,13 +296,15 @@ impl SqliteCalendarStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<CalendarObject, Error> {
|
) -> Result<CalendarObject, Error> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
CalendarObjectRow,
|
CalendarObjectRow,
|
||||||
"SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?)",
|
"SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||||
principal,
|
principal,
|
||||||
cal_id,
|
cal_id,
|
||||||
object_id
|
object_id,
|
||||||
|
show_deleted
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await
|
.await
|
||||||
@@ -454,7 +456,7 @@ impl SqliteCalendarStore {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
for Row { object_id, .. } in changes {
|
for Row { object_id, .. } in changes {
|
||||||
match Self::_get_object(&mut *conn, principal, cal_id, &object_id).await {
|
match Self::_get_object(&mut *conn, principal, cal_id, &object_id, false).await {
|
||||||
Ok(object) => objects.push(object),
|
Ok(object) => objects.push(object),
|
||||||
Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id),
|
Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id),
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
@@ -518,10 +520,8 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
|
|
||||||
if let Some(cal) = cal {
|
if let Some(cal) = cal {
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
r#type: CollectionOperationType::Delete,
|
data: CollectionOperationInfo::Delete,
|
||||||
domain: rustical_store::CollectionOperationDomain::Calendar,
|
|
||||||
topic: cal.push_topic,
|
topic: cal.push_topic,
|
||||||
sync_token: None,
|
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
error!("Push notification about deleted calendar failed: {err}");
|
||||||
};
|
};
|
||||||
@@ -559,8 +559,9 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<CalendarObject, Error> {
|
) -> Result<CalendarObject, Error> {
|
||||||
Self::_get_object(&self.db, principal, cal_id, object_id).await
|
Self::_get_object(&self.db, principal, cal_id, object_id, show_deleted).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@@ -585,7 +586,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let synctoken = log_object_operation(
|
let sync_token = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&principal,
|
&principal,
|
||||||
&cal_id,
|
&cal_id,
|
||||||
@@ -597,10 +598,8 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
r#type: CollectionOperationType::Object,
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
domain: rustical_store::CollectionOperationDomain::Calendar,
|
|
||||||
topic: self.get_calendar(&principal, &cal_id).await?.push_topic,
|
topic: self.get_calendar(&principal, &cal_id).await?.push_topic,
|
||||||
sync_token: Some(synctoken),
|
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
error!("Push notification about deleted calendar failed: {err}");
|
||||||
};
|
};
|
||||||
@@ -619,15 +618,13 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
|
|
||||||
Self::_delete_object(&mut *tx, principal, cal_id, id, use_trashbin).await?;
|
Self::_delete_object(&mut *tx, principal, cal_id, id, use_trashbin).await?;
|
||||||
|
|
||||||
let synctoken =
|
let sync_token =
|
||||||
log_object_operation(&mut tx, principal, cal_id, id, ChangeOperation::Delete).await?;
|
log_object_operation(&mut tx, principal, cal_id, id, ChangeOperation::Delete).await?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
r#type: CollectionOperationType::Object,
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
domain: rustical_store::CollectionOperationDomain::Calendar,
|
|
||||||
topic: self.get_calendar(principal, cal_id).await?.push_topic,
|
topic: self.get_calendar(principal, cal_id).await?.push_topic,
|
||||||
sync_token: Some(synctoken),
|
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
error!("Push notification about deleted calendar failed: {err}");
|
||||||
};
|
};
|
||||||
@@ -645,16 +642,14 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
|
|
||||||
Self::_restore_object(&mut *tx, principal, cal_id, object_id).await?;
|
Self::_restore_object(&mut *tx, principal, cal_id, object_id).await?;
|
||||||
|
|
||||||
let synctoken =
|
let sync_token =
|
||||||
log_object_operation(&mut tx, principal, cal_id, object_id, ChangeOperation::Add)
|
log_object_operation(&mut tx, principal, cal_id, object_id, ChangeOperation::Add)
|
||||||
.await?;
|
.await?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
r#type: CollectionOperationType::Object,
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
domain: rustical_store::CollectionOperationDomain::Calendar,
|
|
||||||
topic: self.get_calendar(principal, cal_id).await?.push_topic,
|
topic: self.get_calendar(principal, cal_id).await?.push_topic,
|
||||||
sync_token: Some(synctoken),
|
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
error!("Push notification about deleted calendar failed: {err}");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ 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
|
||||||
|
|
||||||
- easy to backup, everything saved in one SQLite database
|
- easy to backup, everything saved in one SQLite database
|
||||||
- also export feature in the frontend
|
- also export feature in the frontend
|
||||||
|
- [WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5
|
||||||
- lightweight (the container image contains only one binary)
|
- lightweight (the container image contains only one binary)
|
||||||
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
||||||
- deleted calendars are recoverable
|
- deleted calendars are recoverable
|
||||||
|
|||||||
26
src/app.rs
26
src/app.rs
@@ -34,6 +34,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
|||||||
frontend_config: FrontendConfig,
|
frontend_config: FrontendConfig,
|
||||||
oidc_config: Option<OidcConfig>,
|
oidc_config: Option<OidcConfig>,
|
||||||
nextcloud_login_config: NextcloudLoginConfig,
|
nextcloud_login_config: NextcloudLoginConfig,
|
||||||
|
dav_push_enabled: bool,
|
||||||
) -> Router<()> {
|
) -> Router<()> {
|
||||||
let combined_cal_store = Arc::new(CombinedCalendarStore::new(
|
let combined_cal_store = Arc::new(CombinedCalendarStore::new(
|
||||||
cal_store.clone(),
|
cal_store.clone(),
|
||||||
@@ -90,6 +91,13 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
|||||||
nextcloud_login_router(auth_provider.clone()),
|
nextcloud_login_router(auth_provider.clone()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dav_push_enabled {
|
||||||
|
router = router.merge(rustical_dav_push::subscription_service(
|
||||||
|
subscription_store.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.layer(
|
.layer(
|
||||||
SessionManagerLayer::new(session_store)
|
SessionManagerLayer::new(session_store)
|
||||||
@@ -126,12 +134,18 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
|||||||
if response.status().is_server_error() {
|
if response.status().is_server_error() {
|
||||||
tracing::error!("server error");
|
tracing::error!("server error");
|
||||||
} else if response.status().is_client_error() {
|
} else if response.status().is_client_error() {
|
||||||
if response.status() == StatusCode::UNAUTHORIZED {
|
match response.status() {
|
||||||
// The iOS client always tries an unauthenticated request first so
|
StatusCode::UNAUTHORIZED => {
|
||||||
// logging 401's as errors would clog up our logs
|
// The iOS client always tries an unauthenticated request first so
|
||||||
tracing::debug!("unauthorized");
|
// logging 401's as errors would clog up our logs
|
||||||
} else {
|
tracing::debug!("unauthorized");
|
||||||
tracing::error!("client error");
|
}
|
||||||
|
StatusCode::NOT_FOUND => {
|
||||||
|
tracing::warn!("client error");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::error!("client error");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -37,9 +37,14 @@ pub struct TracingConfig {
|
|||||||
pub opentelemetry: bool,
|
pub opentelemetry: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(deny_unknown_fields, default)]
|
#[serde(deny_unknown_fields, default)]
|
||||||
pub struct DavPushConfig {
|
pub struct DavPushConfig {
|
||||||
|
#[serde(default = "default_true")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
// Allowed Push servers, accepts any by default
|
// Allowed Push servers, accepts any by default
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ async fn main() -> Result<()> {
|
|||||||
config.frontend.clone(),
|
config.frontend.clone(),
|
||||||
config.oidc.clone(),
|
config.oidc.clone(),
|
||||||
config.nextcloud_login.clone(),
|
config.nextcloud_login.clone(),
|
||||||
|
config.dav_push.enabled,
|
||||||
);
|
);
|
||||||
let app = ServiceExt::<Request>::into_make_service(
|
let app = ServiceExt::<Request>::into_make_service(
|
||||||
NormalizePathLayer::trim_trailing_slash().layer(app),
|
NormalizePathLayer::trim_trailing_slash().layer(app),
|
||||||
|
|||||||
Reference in New Issue
Block a user