mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 12:22:16 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79c66a0b46 | ||
|
|
e5687c6e43 | ||
|
|
79b67a17c3 | ||
|
|
7d18faff69 | ||
|
|
753f8e90d3 | ||
|
|
701fa9dd9c | ||
|
|
31b17cfe7f | ||
|
|
d802a0085a | ||
|
|
786b15f5b9 | ||
|
|
f5d097ac55 | ||
|
|
668fa86e3c | ||
|
|
23d2024644 | ||
|
|
15aadcf1be | ||
|
|
4a3b7d7ce6 | ||
|
|
1a2f3b8f8a | ||
|
|
9e8c218308 | ||
|
|
f2adce739b | ||
|
|
0415664ff3 | ||
|
|
677e0082fa | ||
|
|
a387885b0a | ||
|
|
990b953055 | ||
|
|
36b47a645d |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -22,5 +22,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "395e40a7b3333b79bc2ad50a123d99f74bc2712a16257ee2119dd211fdb61f7e"
|
||||
"hash": "246ec675667992c1297c29348d46496a884c59adb8b64b569d36f4ce10f88f47"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -15,12 +15,12 @@
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d2f7423e2e8f97607f6664200990dcadb927445880ec6edffba3b5aedf4e199b"
|
||||
"hash": "543838c030550cb09d1af08adfeade8b7ce3575d92fddbc6e9582d141bc9e49d"
|
||||
}
|
||||
91
Cargo.lock
generated
91
Cargo.lock
generated
@@ -768,7 +768,11 @@ dependencies = [
|
||||
"base64 0.21.7",
|
||||
"byteorder",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"lazy_static",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"sha2",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
@@ -916,6 +920,21 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@@ -1834,6 +1853,54 @@ dependencies = [
|
||||
"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]]
|
||||
name = "opentelemetry"
|
||||
version = "0.30.0"
|
||||
@@ -2669,7 +2736,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -2712,7 +2779,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_caldav"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2747,7 +2814,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_carddav"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2779,7 +2846,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2804,7 +2871,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav_push"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2815,7 +2882,7 @@ dependencies = [
|
||||
"http",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"p256",
|
||||
"openssl",
|
||||
"quick-xml",
|
||||
"rand 0.9.1",
|
||||
"reqwest",
|
||||
@@ -2830,7 +2897,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_frontend"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_web",
|
||||
@@ -2863,7 +2930,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_ical"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -2881,7 +2948,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_oidc"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2896,7 +2963,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2930,7 +2997,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store_sqlite"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -2950,7 +3017,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_xml"
|
||||
version = "0.2.2"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"thiserror 2.0.12",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.0"
|
||||
version = "0.3.6"
|
||||
edition = "2024"
|
||||
description = "A CalDAV server"
|
||||
repository = "https://github.com/lennart-k/rustical"
|
||||
@@ -135,8 +135,10 @@ reqwest = { version = "0.12", features = [
|
||||
openidconnect = "4.0"
|
||||
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 }
|
||||
p256 = { version = "0.13", features = ["ecdh"] }
|
||||
ece = { version = "2.3", default-features = false, features = [
|
||||
"backend-openssl",
|
||||
] }
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
|
||||
[dependencies]
|
||||
rustical_store = { workspace = true }
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN case $TARGETPLATFORM in \
|
||||
*) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \
|
||||
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)" \
|
||||
&& cargo install cargo-chef --locked \
|
||||
&& rm -rf "$CARGO_HOME/registry"
|
||||
|
||||
@@ -4,8 +4,8 @@ a CalDAV/CardDAV server
|
||||
|
||||
> [!WARNING]
|
||||
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 RustiCal for the last few weeks and I'm slowly becoming more confident,
|
||||
however you'd be one of the first testers so expect bugs and rough edges.
|
||||
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
|
||||
|
||||
## Features
|
||||
@@ -30,3 +30,4 @@ a CalDAV/CardDAV server
|
||||
- GNOME Accounts, GNOME Calendar, GNOME Contacts
|
||||
- Evolution
|
||||
- Apple Calendar
|
||||
- Home Assistant integration
|
||||
|
||||
@@ -9,7 +9,7 @@ use ical::generator::{Emitter, IcalCalendarBuilder};
|
||||
use ical::property::Property;
|
||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||
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::str::FromStr;
|
||||
use tracing::instrument;
|
||||
@@ -18,7 +18,7 @@ use tracing::instrument;
|
||||
pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
||||
Path((principal, calendar_id)): Path<(String, String)>,
|
||||
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(crate::Error::Unauthorized);
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::response::{IntoResponse, Response};
|
||||
use http::{Method, StatusCode};
|
||||
use rustical_dav::xml::HrefElement;
|
||||
use rustical_ical::CalendarObjectType;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
|
||||
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
|
||||
use tracing::instrument;
|
||||
@@ -63,7 +63,7 @@ struct MkcolRequest {
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
|
||||
Path((principal, cal_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||
method: Method,
|
||||
body: String,
|
||||
|
||||
@@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderValue, StatusCode, header};
|
||||
use rustical_dav::privileges::UserPrivilege;
|
||||
use rustical_dav::resource::Resource;
|
||||
use rustical_dav_push::register::PushRegister;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
|
||||
use rustical_xml::XmlDocument;
|
||||
use tracing::instrument;
|
||||
@@ -15,7 +15,7 @@ use tracing::instrument;
|
||||
#[instrument(skip(resource_service))]
|
||||
pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
|
||||
Path((principal, cal_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
State(resource_service): State<CalendarResourceService<C, S>>,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
|
||||
@@ -29,7 +29,7 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
||||
if let Some(filename) = href.strip_prefix(path) {
|
||||
let filename = filename.trim_start_matches("/");
|
||||
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),
|
||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
||||
Err(err) => return Err(err.into()),
|
||||
|
||||
@@ -21,7 +21,7 @@ use rustical_dav::{
|
||||
},
|
||||
};
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::{CalendarStore, SubscriptionStore, auth::User};
|
||||
use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
|
||||
use rustical_xml::{XmlDeserialize, XmlDocument};
|
||||
use sync_collection::handle_sync_collection;
|
||||
use tracing::instrument;
|
||||
@@ -56,7 +56,7 @@ fn objects_response(
|
||||
path: &str,
|
||||
principal: &str,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
prop: &PropfindType<CalendarObjectPropWrapperName>,
|
||||
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
|
||||
let mut responses = Vec::new();
|
||||
@@ -90,7 +90,7 @@ fn objects_response(
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn route_report_calendar<C: CalendarStore, S: SubscriptionStore>(
|
||||
Path((principal, cal_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
Extension(puri): Extension<CalDavPrincipalUri>,
|
||||
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
|
||||
@@ -13,7 +13,7 @@ use rustical_dav::{
|
||||
};
|
||||
use rustical_store::{
|
||||
CalendarStore,
|
||||
auth::User,
|
||||
auth::Principal,
|
||||
synctoken::{format_synctoken, parse_synctoken},
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn handle_sync_collection<C: CalendarStore>(
|
||||
sync_collection: &SyncCollectionRequest<CalendarObjectPropWrapperName>,
|
||||
path: &str,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
cal_store: &C,
|
||||
|
||||
@@ -12,7 +12,7 @@ use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedR
|
||||
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
|
||||
use rustical_ical::CalDateTime;
|
||||
use rustical_store::Calendar;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
use rustical_xml::{EnumVariants, PropName};
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
use std::str::FromStr;
|
||||
@@ -95,7 +95,7 @@ impl DavPushExtension for CalendarResource {
|
||||
impl Resource for CalendarResource {
|
||||
type Prop = CalendarPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
|
||||
fn is_collection(&self) -> bool {
|
||||
true
|
||||
@@ -121,7 +121,7 @@ impl Resource for CalendarResource {
|
||||
fn get_prop(
|
||||
&self,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
prop: &CalendarPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
Ok(match prop {
|
||||
@@ -291,8 +291,13 @@ impl Resource for CalendarResource {
|
||||
Some(&self.cal.principal)
|
||||
}
|
||||
|
||||
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> {
|
||||
if self.cal.subscription_url.is_some() || self.read_only {
|
||||
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
||||
if self.cal.subscription_url.is_some() {
|
||||
return Ok(UserPrivilegeSet::owner_write_properties(
|
||||
user.is_principal(&self.cal.principal),
|
||||
));
|
||||
}
|
||||
if self.read_only {
|
||||
return Ok(UserPrivilegeSet::owner_read(
|
||||
user.is_principal(&self.cal.principal),
|
||||
));
|
||||
|
||||
@@ -13,7 +13,7 @@ use axum::handler::Handler;
|
||||
use axum::response::Response;
|
||||
use futures_util::future::BoxFuture;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
use rustical_store::{CalendarStore, SubscriptionStore};
|
||||
use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
@@ -48,7 +48,7 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
|
||||
type PathComponents = (String, String); // principal, calendar_id
|
||||
type Resource = CalendarResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy, webdav-push";
|
||||
|
||||
@@ -9,7 +9,7 @@ use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
use std::str::FromStr;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn get_event<C: CalendarStore>(
|
||||
object_id,
|
||||
}): Path<CalendarObjectPathComponents>,
|
||||
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(crate::Error::Unauthorized);
|
||||
@@ -33,7 +33,7 @@ pub async fn get_event<C: CalendarStore>(
|
||||
}
|
||||
|
||||
let event = cal_store
|
||||
.get_object(&principal, &calendar_id, &object_id)
|
||||
.get_object(&principal, &calendar_id, &object_id, false)
|
||||
.await?;
|
||||
|
||||
let mut resp = Response::builder().status(StatusCode::OK);
|
||||
@@ -51,7 +51,7 @@ pub async fn put_event<C: CalendarStore>(
|
||||
object_id,
|
||||
}): Path<CalendarObjectPathComponents>,
|
||||
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
header_map: HeaderMap,
|
||||
body: String,
|
||||
|
||||
@@ -8,7 +8,7 @@ use rustical_dav::{
|
||||
xml::Resourcetype,
|
||||
};
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
|
||||
#[derive(Clone, From, Into)]
|
||||
pub struct CalendarObjectResource {
|
||||
@@ -25,7 +25,7 @@ impl ResourceName for CalendarObjectResource {
|
||||
impl Resource for CalendarObjectResource {
|
||||
type Prop = CalendarObjectPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
|
||||
fn is_collection(&self) -> bool {
|
||||
false
|
||||
@@ -38,7 +38,7 @@ impl Resource for CalendarObjectResource {
|
||||
fn get_prop(
|
||||
&self,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
prop: &CalendarObjectPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
Ok(match prop {
|
||||
@@ -81,7 +81,7 @@ impl Resource for CalendarObjectResource {
|
||||
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(
|
||||
user.is_principal(&self.principal),
|
||||
))
|
||||
|
||||
@@ -9,7 +9,7 @@ use async_trait::async_trait;
|
||||
use axum::{extract::Request, handler::Handler, response::Response};
|
||||
use futures_util::future::BoxFuture;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::{CalendarStore, auth::User};
|
||||
use rustical_store::{CalendarStore, auth::Principal};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::{convert::Infallible, sync::Arc};
|
||||
use tower::Service;
|
||||
@@ -46,7 +46,7 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
||||
type Resource = CalendarObjectResource;
|
||||
type MemberType = CalendarObjectResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
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> {
|
||||
let object = self
|
||||
.cal_store
|
||||
.get_object(principal, calendar_id, object_id)
|
||||
.get_object(principal, calendar_id, object_id, false)
|
||||
.await?;
|
||||
Ok(CalendarObjectResource {
|
||||
object,
|
||||
|
||||
@@ -6,7 +6,7 @@ use principal::PrincipalResourceService;
|
||||
use rustical_dav::resource::{PrincipalUri, ResourceService};
|
||||
use rustical_dav::resources::RootResourceService;
|
||||
use rustical_store::auth::middleware::AuthenticationLayer;
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal};
|
||||
use rustical_store::{CalendarStore, SubscriptionStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -44,7 +44,7 @@ pub fn caldav_router<AP: AuthenticationProvider, C: CalendarStore, S: Subscripti
|
||||
Router::new()
|
||||
.nest(
|
||||
prefix,
|
||||
RootResourceService::<_, User, CalDavPrincipalUri>::new(principal_service.clone())
|
||||
RootResourceService::<_, Principal, CalDavPrincipalUri>::new(principal_service.clone())
|
||||
.axum_router()
|
||||
.layer(AuthenticationLayer::new(auth_provider))
|
||||
.layer(Extension(CalDavPrincipalUri(prefix))),
|
||||
|
||||
@@ -5,7 +5,7 @@ use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||
use rustical_dav::xml::{
|
||||
GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet,
|
||||
};
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
|
||||
mod service;
|
||||
pub use service::*;
|
||||
@@ -14,7 +14,7 @@ pub use prop::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PrincipalResource {
|
||||
principal: User,
|
||||
principal: Principal,
|
||||
members: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ impl ResourceName for PrincipalResource {
|
||||
impl Resource for PrincipalResource {
|
||||
type Prop = PrincipalPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
|
||||
fn is_collection(&self) -> bool {
|
||||
true
|
||||
@@ -38,17 +38,17 @@ impl Resource for PrincipalResource {
|
||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
|
||||
// https://github.com/apple/ccs-calendarserver/blob/13c706b985fb728b9aab42dc0fef85aae21921c3/doc/Extensions/caldav-proxy.txt
|
||||
ResourcetypeInner(
|
||||
Some(rustical_dav::namespace::NS_CALENDARSERVER),
|
||||
"calendar-proxy-write",
|
||||
),
|
||||
// ResourcetypeInner(
|
||||
// Some(rustical_dav::namespace::NS_CALENDARSERVER),
|
||||
// "calendar-proxy-write",
|
||||
// ),
|
||||
])
|
||||
}
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
prop: &PrincipalPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
let principal_url = puri.principal_uri(&self.principal.id);
|
||||
@@ -113,7 +113,7 @@ impl Resource for PrincipalResource {
|
||||
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(
|
||||
user.is_principal(&self.principal.id),
|
||||
))
|
||||
|
||||
@@ -2,7 +2,7 @@ use rustical_dav::{
|
||||
extensions::CommonPropertiesProp,
|
||||
xml::{GroupMemberSet, GroupMembership, HrefElement, SupportedReportSet},
|
||||
};
|
||||
use rustical_store::auth::user::PrincipalType;
|
||||
use rustical_store::auth::PrincipalType;
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
use strum_macros::VariantArray;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{CalDavPrincipalUri, Error};
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal};
|
||||
use rustical_store::{CalendarStore, SubscriptionStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -40,7 +40,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
|
||||
type MemberType = CalendarResource;
|
||||
type Resource = PrincipalResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";
|
||||
|
||||
@@ -12,7 +12,7 @@ use rustical_dav::privileges::UserPrivilege;
|
||||
use rustical_dav::resource::Resource;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::AddressbookStore;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
use std::str::FromStr;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -24,7 +24,7 @@ pub async fn get_object<AS: AddressbookStore>(
|
||||
object_id,
|
||||
}): Path<AddressObjectPathComponents>,
|
||||
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
@@ -60,7 +60,7 @@ pub async fn put_object<AS: AddressbookStore>(
|
||||
object_id,
|
||||
}): Path<AddressObjectPathComponents>,
|
||||
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
header_map: HeaderMap,
|
||||
body: String,
|
||||
|
||||
@@ -13,7 +13,7 @@ use rustical_dav::{
|
||||
xml::Resourcetype,
|
||||
};
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
|
||||
#[derive(Clone, From, Into)]
|
||||
pub struct AddressObjectResource {
|
||||
@@ -30,7 +30,7 @@ impl ResourceName for AddressObjectResource {
|
||||
impl Resource for AddressObjectResource {
|
||||
type Prop = AddressObjectPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
|
||||
fn is_collection(&self) -> bool {
|
||||
false
|
||||
@@ -43,7 +43,7 @@ impl Resource for AddressObjectResource {
|
||||
fn get_prop(
|
||||
&self,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
prop: &AddressObjectPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
Ok(match prop {
|
||||
@@ -78,7 +78,7 @@ impl Resource for AddressObjectResource {
|
||||
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(
|
||||
user.is_principal(&self.principal),
|
||||
))
|
||||
|
||||
@@ -5,7 +5,7 @@ use axum::{extract::Request, handler::Handler, response::Response};
|
||||
use derive_more::derive::Constructor;
|
||||
use futures_util::future::BoxFuture;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::{AddressbookStore, auth::User};
|
||||
use rustical_store::{AddressbookStore, auth::Principal};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::{convert::Infallible, sync::Arc};
|
||||
use tower::Service;
|
||||
@@ -37,7 +37,7 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
|
||||
type Resource = AddressObjectResource;
|
||||
type MemberType = AddressObjectResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
type PrincipalUri = CardDavPrincipalUri;
|
||||
|
||||
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::resource::Resource;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore};
|
||||
use std::str::FromStr;
|
||||
use tracing::instrument;
|
||||
@@ -19,7 +19,7 @@ use tracing::instrument;
|
||||
pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
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 tracing::instrument;
|
||||
|
||||
@@ -44,7 +44,7 @@ struct MkcolRequest {
|
||||
#[instrument(skip(addr_store))]
|
||||
pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
|
||||
@@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderValue, StatusCode, header};
|
||||
use rustical_dav::privileges::UserPrivilege;
|
||||
use rustical_dav::resource::Resource;
|
||||
use rustical_dav_push::register::PushRegister;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
use rustical_store::{AddressbookStore, Subscription, SubscriptionStore};
|
||||
use rustical_xml::XmlDocument;
|
||||
use tracing::instrument;
|
||||
@@ -15,7 +15,7 @@ use tracing::instrument;
|
||||
#[instrument(skip(resource_service))]
|
||||
pub async fn route_post<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addr_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
State(resource_service): State<AddressbookResourceService<AS, S>>,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
|
||||
@@ -9,14 +9,14 @@ use http::StatusCode;
|
||||
use ical::VcardParser;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::Addressbook;
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore, auth::User};
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(addr_store))]
|
||||
pub async fn route_put<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
|
||||
@@ -10,7 +10,7 @@ use rustical_dav::{
|
||||
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},
|
||||
};
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::{AddressbookStore, auth::User};
|
||||
use rustical_store::{AddressbookStore, auth::Principal};
|
||||
use rustical_xml::XmlDeserialize;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
@@ -63,7 +63,7 @@ pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
|
||||
prop: &PropfindType<AddressObjectPropWrapperName>,
|
||||
path: &str,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
addr_store: &AS,
|
||||
|
||||
@@ -9,7 +9,7 @@ use axum::{
|
||||
response::IntoResponse,
|
||||
};
|
||||
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 sync_collection::handle_sync_collection;
|
||||
use tracing::instrument;
|
||||
@@ -37,7 +37,7 @@ impl ReportRequest {
|
||||
#[instrument(skip(addr_store))]
|
||||
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
Extension(puri): Extension<CardDavPrincipalUri>,
|
||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||
|
||||
@@ -13,7 +13,7 @@ use rustical_dav::{
|
||||
};
|
||||
use rustical_store::{
|
||||
AddressbookStore,
|
||||
auth::User,
|
||||
auth::Principal,
|
||||
synctoken::{format_synctoken, parse_synctoken},
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn handle_sync_collection<AS: AddressbookStore>(
|
||||
sync_collection: &SyncCollectionRequest<AddressObjectPropWrapperName>,
|
||||
path: &str,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
addr_store: &AS,
|
||||
|
||||
@@ -10,7 +10,7 @@ use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||
use rustical_dav::xml::{Resourcetype, ResourcetypeInner, SupportedReportSet};
|
||||
use rustical_dav_push::DavPushExtension;
|
||||
use rustical_store::Addressbook;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
|
||||
#[derive(Clone, Debug, From, Into)]
|
||||
pub struct AddressbookResource(pub(crate) Addressbook);
|
||||
@@ -36,7 +36,7 @@ impl DavPushExtension for AddressbookResource {
|
||||
impl Resource for AddressbookResource {
|
||||
type Prop = AddressbookPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
|
||||
fn is_collection(&self) -> bool {
|
||||
true
|
||||
@@ -52,7 +52,7 @@ impl Resource for AddressbookResource {
|
||||
fn get_prop(
|
||||
&self,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
prop: &AddressbookPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
Ok(match prop {
|
||||
@@ -138,7 +138,7 @@ impl Resource for AddressbookResource {
|
||||
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(
|
||||
user.is_principal(&self.0.principal),
|
||||
))
|
||||
|
||||
@@ -14,7 +14,7 @@ use axum::handler::Handler;
|
||||
use axum::response::Response;
|
||||
use futures_util::future::BoxFuture;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore};
|
||||
use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
@@ -51,7 +51,7 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
|
||||
type PathComponents = (String, String); // principal, addressbook_id
|
||||
type Resource = AddressbookResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
type PrincipalUri = CardDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, addressbook, webdav-push";
|
||||
|
||||
@@ -9,7 +9,7 @@ use rustical_dav::resources::RootResourceService;
|
||||
use rustical_store::auth::middleware::AuthenticationLayer;
|
||||
use rustical_store::{
|
||||
AddressbookStore, SubscriptionStore,
|
||||
auth::{AuthenticationProvider, User},
|
||||
auth::{AuthenticationProvider, Principal},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -44,10 +44,12 @@ pub fn carddav_router<AP: AuthenticationProvider, A: AddressbookStore, S: Subscr
|
||||
Router::new()
|
||||
.nest(
|
||||
prefix,
|
||||
RootResourceService::<_, User, CardDavPrincipalUri>::new(principal_service.clone())
|
||||
.axum_router()
|
||||
.layer(AuthenticationLayer::new(auth_provider))
|
||||
.layer(Extension(CardDavPrincipalUri(prefix))),
|
||||
RootResourceService::<_, Principal, CardDavPrincipalUri>::new(
|
||||
principal_service.clone(),
|
||||
)
|
||||
.axum_router()
|
||||
.layer(AuthenticationLayer::new(auth_provider))
|
||||
.layer(Extension(CardDavPrincipalUri(prefix))),
|
||||
)
|
||||
.route(
|
||||
"/.well-known/carddav",
|
||||
|
||||
@@ -5,7 +5,7 @@ use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||
use rustical_dav::xml::{
|
||||
GroupMemberSet, GroupMembership, HrefElement, Resourcetype, ResourcetypeInner,
|
||||
};
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::auth::Principal;
|
||||
|
||||
mod service;
|
||||
pub use service::*;
|
||||
@@ -14,7 +14,7 @@ pub use prop::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrincipalResource {
|
||||
principal: User,
|
||||
principal: Principal,
|
||||
members: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ impl ResourceName for PrincipalResource {
|
||||
impl Resource for PrincipalResource {
|
||||
type Prop = PrincipalPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
|
||||
fn is_collection(&self) -> bool {
|
||||
true
|
||||
@@ -43,7 +43,7 @@ impl Resource for PrincipalResource {
|
||||
fn get_prop(
|
||||
&self,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
user: &Principal,
|
||||
prop: &PrincipalPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
let principal_href = HrefElement::new(puri.principal_uri(&self.principal.id));
|
||||
@@ -99,7 +99,7 @@ impl Resource for PrincipalResource {
|
||||
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(
|
||||
user.is_principal(&self.principal.id),
|
||||
))
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{CardDavPrincipalUri, Error};
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal};
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -51,7 +51,7 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
|
||||
type MemberType = AddressbookResource;
|
||||
type Resource = PrincipalResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type Principal = Principal;
|
||||
type PrincipalUri = CardDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
|
||||
|
||||
@@ -2,6 +2,7 @@ use quick_xml::name::Namespace;
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc3744
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq)]
|
||||
pub enum UserPrivilege {
|
||||
Read,
|
||||
@@ -47,6 +48,12 @@ pub struct UserPrivilegeSet {
|
||||
|
||||
impl UserPrivilegeSet {
|
||||
pub fn has(&self, privilege: &UserPrivilege) -> bool {
|
||||
if (privilege == &UserPrivilege::WriteProperties
|
||||
|| privilege == &UserPrivilege::WriteContent)
|
||||
&& self.privileges.contains(&UserPrivilege::Write)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All)
|
||||
}
|
||||
|
||||
@@ -72,6 +79,15 @@ impl UserPrivilegeSet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn owner_write_properties(is_owner: bool) -> Self {
|
||||
// Content is read-only but we can write properties
|
||||
if is_owner {
|
||||
Self::write_properties()
|
||||
} else {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_only() -> Self {
|
||||
Self {
|
||||
privileges: HashSet::from([
|
||||
@@ -81,6 +97,17 @@ impl UserPrivilegeSet {
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_properties() -> Self {
|
||||
Self {
|
||||
privileges: HashSet::from([
|
||||
UserPrivilege::Read,
|
||||
UserPrivilege::WriteProperties,
|
||||
UserPrivilege::ReadAcl,
|
||||
UserPrivilege::ReadCurrentUserPrivilegeSet,
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> From<[UserPrivilege; N]> for UserPrivilegeSet {
|
||||
|
||||
@@ -47,8 +47,9 @@ pub async fn route_delete<R: ResourceService>(
|
||||
) -> Result<(), R::Error> {
|
||||
let resource = resource_service.get_resource(path_components).await?;
|
||||
|
||||
// Kind of a bodge since we don't get unbind from the parent
|
||||
let privileges = resource.get_user_privileges(principal)?;
|
||||
if !privileges.has(&UserPrivilege::Write) {
|
||||
if !privileges.has(&UserPrivilege::WriteProperties) {
|
||||
return Err(Error::Unauthorized.into());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::xml::TagList;
|
||||
use headers::{CacheControl, ContentType, HeaderMapExt};
|
||||
use http::StatusCode;
|
||||
use quick_xml::name::Namespace;
|
||||
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 {
|
||||
use axum::body::Body;
|
||||
use http::header;
|
||||
|
||||
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);
|
||||
@@ -118,9 +118,9 @@ impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
|
||||
}
|
||||
|
||||
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
|
||||
resp.headers_mut()
|
||||
.unwrap()
|
||||
.insert(header::CONTENT_TYPE, "application/xml".try_into().unwrap());
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ContentType::xml());
|
||||
hdrs.typed_insert(CacheControl::new().with_no_cache());
|
||||
resp.body(Body::from(output)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ rustical_dav.workspace = true
|
||||
rustical_store.workspace = true
|
||||
http.workspace = true
|
||||
base64.workspace = true
|
||||
p256.workspace = true
|
||||
rand.workspace = true
|
||||
ece.workspace = true
|
||||
axum.workspace = true
|
||||
openssl.workspace = true
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"ES2024",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||
import { createClient } from "webdav";
|
||||
|
||||
@customElement("create-addressbook-form")
|
||||
@@ -24,12 +25,15 @@ export class CreateAddressbookForm extends LitElement {
|
||||
@property()
|
||||
description: String = ''
|
||||
|
||||
dialog: Ref<HTMLDialogElement> = createRef()
|
||||
form: Ref<HTMLFormElement> = createRef()
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<section>
|
||||
<h3>Create calendar</h3>
|
||||
<form @submit=${this.submit}>
|
||||
<button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
|
||||
<dialog ${ref(this.dialog)}>
|
||||
<h3>Create addressbook</h3>
|
||||
<form @submit=${this.submit} ${ref(this.form)}>
|
||||
<label>
|
||||
id
|
||||
<input type="text" name="id" @change=${e => this.id = e.target.value} />
|
||||
@@ -46,8 +50,9 @@ export class CreateAddressbookForm extends LitElement {
|
||||
</label>
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
|
||||
</form>
|
||||
</section>
|
||||
</dialog>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||
import { createClient } from "webdav";
|
||||
|
||||
@customElement("create-calendar-form")
|
||||
@@ -30,12 +31,16 @@ export class CreateCalendarForm extends LitElement {
|
||||
@property()
|
||||
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
|
||||
|
||||
dialog: Ref<HTMLDialogElement> = createRef()
|
||||
form: Ref<HTMLFormElement> = createRef()
|
||||
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<section>
|
||||
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
||||
<dialog ${ref(this.dialog)}>
|
||||
<h3>Create calendar</h3>
|
||||
<form @submit=${this.submit}>
|
||||
<form @submit=${this.submit} ${ref(this.form)}>
|
||||
<label>
|
||||
id
|
||||
<input type="text" name="id" @change=${e => this.id = e.target.value} />
|
||||
@@ -69,9 +74,10 @@ export class CreateCalendarForm extends LitElement {
|
||||
`)}
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</section>
|
||||
`
|
||||
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
|
||||
</form>
|
||||
</dialog>
|
||||
`
|
||||
}
|
||||
|
||||
async submit(e: SubmitEvent) {
|
||||
@@ -97,7 +103,7 @@ export class CreateCalendarForm extends LitElement {
|
||||
<displayname>${this.displayname}</displayname>
|
||||
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''}
|
||||
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''}
|
||||
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ''}
|
||||
${this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ''}
|
||||
<CAL:supported-calendar-component-set>
|
||||
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')}
|
||||
</CAL:supported-calendar-component-set>
|
||||
|
||||
44
crates/frontend/js-components/lib/delete-button.ts
Normal file
44
crates/frontend/js-components/lib/delete-button.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { createClient } from "webdav";
|
||||
|
||||
@customElement("delete-button")
|
||||
export class DeleteButton extends LitElement {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
trash: boolean = false
|
||||
@property()
|
||||
href: string
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this
|
||||
}
|
||||
|
||||
protected render() {
|
||||
let text = this.trash ? 'Move to trash' : 'Delete'
|
||||
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
|
||||
}
|
||||
|
||||
async _onClick(event: Event) {
|
||||
event.preventDefault()
|
||||
if (!this.trash && !confirm('Do you want to delete this collection permanently?')) {
|
||||
return
|
||||
}
|
||||
|
||||
let response = await fetch(this.href, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-No-Trashbin': this.trash ? '0' : '1'
|
||||
}
|
||||
})
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
alert('An error occured, look into the console')
|
||||
console.error(response)
|
||||
return
|
||||
}
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"target": "es2024",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false
|
||||
"useDefineForClassFields": false,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2024"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"lib/**/*.ts"
|
||||
|
||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
||||
input: [
|
||||
"lib/create-calendar-form.ts",
|
||||
"lib/create-addressbook-form.ts",
|
||||
"lib/delete-button.ts",
|
||||
],
|
||||
output: {
|
||||
dir: "../public/assets/js/",
|
||||
|
||||
@@ -1,45 +1,50 @@
|
||||
import { i as d, x as m } from "./lit-Dq9MfRDi.mjs";
|
||||
import { n, t as c } from "./property-DwhV4xIV.mjs";
|
||||
import { a as u } from "./webdav-Bz4I5vNH.mjs";
|
||||
var h = Object.defineProperty, y = Object.getOwnPropertyDescriptor, r = (e, a, o, s) => {
|
||||
for (var t = s > 1 ? void 0 : s ? y(a, o) : a, p = e.length - 1, l; p >= 0; p--)
|
||||
(l = e[p]) && (t = (s ? l(a, o, t) : l(t)) || t);
|
||||
return s && t && h(a, o, t), t;
|
||||
import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
|
||||
import { n as o, t as h } from "./property-DYFkTqgI.mjs";
|
||||
import { e as d, n as m } from "./ref-nf9JiOyl.mjs";
|
||||
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
||||
var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, r = (t, a, n, s) => {
|
||||
for (var e = s > 1 ? void 0 : s ? f(a, n) : a, l = t.length - 1, p; l >= 0; l--)
|
||||
(p = t[l]) && (e = (s ? p(a, n, e) : p(e)) || e);
|
||||
return s && e && y(a, n, e), e;
|
||||
};
|
||||
let i = class extends d {
|
||||
let i = class extends c {
|
||||
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() {
|
||||
return this;
|
||||
}
|
||||
render() {
|
||||
return m`
|
||||
<section>
|
||||
<h3>Create calendar</h3>
|
||||
<form @submit=${this.submit}>
|
||||
return u`
|
||||
<button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
|
||||
<dialog ${m(this.dialog)}>
|
||||
<h3>Create addressbook</h3>
|
||||
<form @submit=${this.submit} ${m(this.form)}>
|
||||
<label>
|
||||
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>
|
||||
<br>
|
||||
<label>
|
||||
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>
|
||||
<br>
|
||||
<label>
|
||||
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>
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
<button type="submit" @click=${(t) => {
|
||||
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
|
||||
}}> Cancel </button>
|
||||
</form>
|
||||
</section>
|
||||
</dialog>
|
||||
`;
|
||||
}
|
||||
async submit(e) {
|
||||
if (console.log(this.displayname), e.preventDefault(), !this.id) {
|
||||
async submit(t) {
|
||||
if (console.log(this.displayname), t.preventDefault(), !this.id) {
|
||||
alert("Empty id");
|
||||
return;
|
||||
}
|
||||
@@ -62,19 +67,19 @@ let i = class extends d {
|
||||
}
|
||||
};
|
||||
r([
|
||||
n()
|
||||
o()
|
||||
], i.prototype, "user", 2);
|
||||
r([
|
||||
n()
|
||||
o()
|
||||
], i.prototype, "id", 2);
|
||||
r([
|
||||
n()
|
||||
o()
|
||||
], i.prototype, "displayname", 2);
|
||||
r([
|
||||
n()
|
||||
o()
|
||||
], i.prototype, "description", 2);
|
||||
i = r([
|
||||
c("create-addressbook-form")
|
||||
h("create-addressbook-form")
|
||||
], i);
|
||||
export {
|
||||
i as CreateAddressbookForm
|
||||
|
||||
@@ -1,62 +1,67 @@
|
||||
import { i as m, x as c } from "./lit-Dq9MfRDi.mjs";
|
||||
import { n as s, t as d } from "./property-DwhV4xIV.mjs";
|
||||
import { a as u } from "./webdav-Bz4I5vNH.mjs";
|
||||
var h = Object.defineProperty, b = Object.getOwnPropertyDescriptor, a = (e, t, o, n) => {
|
||||
for (var i = n > 1 ? void 0 : n ? b(t, o) : t, l = e.length - 1, p; l >= 0; l--)
|
||||
(p = e[l]) && (i = (n ? p(t, o, i) : p(i)) || i);
|
||||
return n && i && h(t, o, i), i;
|
||||
import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
|
||||
import { n as o, t as h } from "./property-DYFkTqgI.mjs";
|
||||
import { e as m, n as d } from "./ref-nf9JiOyl.mjs";
|
||||
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
||||
var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, n, s) => {
|
||||
for (var i = s > 1 ? void 0 : s ? $(e, n) : e, l = t.length - 1, p; l >= 0; l--)
|
||||
(p = t[l]) && (i = (s ? p(e, n, i) : p(i)) || i);
|
||||
return s && i && y(e, n, i), i;
|
||||
};
|
||||
let r = class extends m {
|
||||
let r = class extends u {
|
||||
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 = m(), this.form = m();
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
render() {
|
||||
return c`
|
||||
<section>
|
||||
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
||||
<dialog ${d(this.dialog)}>
|
||||
<h3>Create calendar</h3>
|
||||
<form @submit=${this.submit}>
|
||||
<form @submit=${this.submit} ${d(this.form)}>
|
||||
<label>
|
||||
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>
|
||||
<br>
|
||||
<label>
|
||||
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>
|
||||
<br>
|
||||
<label>
|
||||
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>
|
||||
<br>
|
||||
<label>
|
||||
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>
|
||||
<br>
|
||||
<label>
|
||||
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>
|
||||
<br>
|
||||
${["VEVENT", "VTODO", "VJOURNAL"].map((e) => c`
|
||||
${["VEVENT", "VTODO", "VJOURNAL"].map((t) => c`
|
||||
<label>
|
||||
Support ${e}
|
||||
<input type="checkbox" value=${e} @change=${(t) => t.target.checked ? this.components.add(t.target.value) : this.components.delete(t.target.value)} />
|
||||
Support ${t}
|
||||
<input type="checkbox" value=${t} @change=${(e) => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
||||
</label>
|
||||
`)}
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</section>
|
||||
`;
|
||||
<button type="submit" @click=${(t) => {
|
||||
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
|
||||
}}> Cancel </button>
|
||||
</form>
|
||||
</dialog>
|
||||
`;
|
||||
}
|
||||
async submit(e) {
|
||||
if (console.log(this.displayname), e.preventDefault(), !this.id) {
|
||||
async submit(t) {
|
||||
if (console.log(this.displayname), t.preventDefault(), !this.id) {
|
||||
alert("Empty id");
|
||||
return;
|
||||
}
|
||||
@@ -76,9 +81,9 @@ let r = class extends m {
|
||||
<displayname>${this.displayname}</displayname>
|
||||
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ""}
|
||||
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""}
|
||||
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ""}
|
||||
${this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ""}
|
||||
<CAL:supported-calendar-component-set>
|
||||
${Array.from(this.components.keys()).map((t) => `<CAL:comp name="${t}" />`).join(`
|
||||
${Array.from(this.components.keys()).map((e) => `<CAL:comp name="${e}" />`).join(`
|
||||
`)}
|
||||
</CAL:supported-calendar-component-set>
|
||||
</prop>
|
||||
@@ -89,28 +94,28 @@ let r = class extends m {
|
||||
}
|
||||
};
|
||||
a([
|
||||
s()
|
||||
o()
|
||||
], r.prototype, "user", 2);
|
||||
a([
|
||||
s()
|
||||
o()
|
||||
], r.prototype, "id", 2);
|
||||
a([
|
||||
s()
|
||||
o()
|
||||
], r.prototype, "displayname", 2);
|
||||
a([
|
||||
s()
|
||||
o()
|
||||
], r.prototype, "description", 2);
|
||||
a([
|
||||
s()
|
||||
o()
|
||||
], r.prototype, "color", 2);
|
||||
a([
|
||||
s()
|
||||
o()
|
||||
], r.prototype, "subscriptionUrl", 2);
|
||||
a([
|
||||
s()
|
||||
o()
|
||||
], r.prototype, "components", 2);
|
||||
r = a([
|
||||
d("create-calendar-form")
|
||||
h("create-calendar-form")
|
||||
], r);
|
||||
export {
|
||||
r as CreateCalendarForm
|
||||
|
||||
46
crates/frontend/public/assets/js/delete-button.mjs
Normal file
46
crates/frontend/public/assets/js/delete-button.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
import { i as c, x as p } from "./lit-CWlWuEHk.mjs";
|
||||
import { n as h, t as u } from "./property-DYFkTqgI.mjs";
|
||||
var f = Object.defineProperty, d = Object.getOwnPropertyDescriptor, i = (r, t, n, o) => {
|
||||
for (var e = o > 1 ? void 0 : o ? d(t, n) : t, l = r.length - 1, a; l >= 0; l--)
|
||||
(a = r[l]) && (e = (o ? a(t, n, e) : a(e)) || e);
|
||||
return o && e && f(t, n, e), e;
|
||||
};
|
||||
let s = class extends c {
|
||||
constructor() {
|
||||
super(), this.trash = !1;
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
render() {
|
||||
let r = this.trash ? "Move to trash" : "Delete";
|
||||
return p`<button class="delete" @click=${(t) => this._onClick(t)}>${r}</button>`;
|
||||
}
|
||||
async _onClick(r) {
|
||||
if (r.preventDefault(), !this.trash && !confirm("Do you want to delete this collection permanently?"))
|
||||
return;
|
||||
let t = await fetch(this.href, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-No-Trashbin": this.trash ? "0" : "1"
|
||||
}
|
||||
});
|
||||
if (t.status < 200 || t.status >= 300) {
|
||||
alert("An error occured, look into the console"), console.error(t);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
i([
|
||||
h({ type: Boolean })
|
||||
], s.prototype, "trash", 2);
|
||||
i([
|
||||
h()
|
||||
], s.prototype, "href", 2);
|
||||
s = i([
|
||||
u("delete-button")
|
||||
], s);
|
||||
export {
|
||||
s as DeleteButton
|
||||
};
|
||||
@@ -543,6 +543,7 @@ const z = y.litElementPolyfillSupport;
|
||||
z == null || z({ LitElement: T });
|
||||
(y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0");
|
||||
export {
|
||||
d as E,
|
||||
et as f,
|
||||
T as i,
|
||||
j as u,
|
||||
@@ -1,4 +1,4 @@
|
||||
import { f as d, u as l } from "./lit-Dq9MfRDi.mjs";
|
||||
import { f as d, u as l } from "./lit-CWlWuEHk.mjs";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
128
crates/frontend/public/assets/js/ref-nf9JiOyl.mjs
Normal file
128
crates/frontend/public/assets/js/ref-nf9JiOyl.mjs
Normal file
@@ -0,0 +1,128 @@
|
||||
import { E as $ } from "./lit-CWlWuEHk.mjs";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const d = (t) => t.strings === void 0;
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const _ = { CHILD: 2 }, a = (t) => (...s) => ({ _$litDirective$: t, values: s });
|
||||
class A {
|
||||
constructor(s) {
|
||||
}
|
||||
get _$AU() {
|
||||
return this._$AM._$AU;
|
||||
}
|
||||
_$AT(s, e, i) {
|
||||
this._$Ct = s, this._$AM = e, this._$Ci = i;
|
||||
}
|
||||
_$AS(s, e) {
|
||||
return this.update(s, e);
|
||||
}
|
||||
update(s, e) {
|
||||
return this.render(...e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const n = (t, s) => {
|
||||
var i;
|
||||
const e = t._$AN;
|
||||
if (e === void 0) return !1;
|
||||
for (const h of e) (i = h._$AO) == null || i.call(h, s, !1), n(h, s);
|
||||
return !0;
|
||||
}, r = (t) => {
|
||||
let s, e;
|
||||
do {
|
||||
if ((s = t._$AM) === void 0) break;
|
||||
e = s._$AN, e.delete(t), t = s;
|
||||
} while ((e == null ? void 0 : e.size) === 0);
|
||||
}, l = (t) => {
|
||||
for (let s; s = t._$AM; t = s) {
|
||||
let e = s._$AN;
|
||||
if (e === void 0) s._$AN = e = /* @__PURE__ */ new Set();
|
||||
else if (e.has(t)) break;
|
||||
e.add(t), v(s);
|
||||
}
|
||||
};
|
||||
function f(t) {
|
||||
this._$AN !== void 0 ? (r(this), this._$AM = t, l(this)) : this._$AM = t;
|
||||
}
|
||||
function u(t, s = !1, e = 0) {
|
||||
const i = this._$AH, h = this._$AN;
|
||||
if (h !== void 0 && h.size !== 0) if (s) if (Array.isArray(i)) for (let o = e; o < i.length; o++) n(i[o], !1), r(i[o]);
|
||||
else i != null && (n(i, !1), r(i));
|
||||
else n(this, t);
|
||||
}
|
||||
const v = (t) => {
|
||||
t.type == _.CHILD && (t._$AP ?? (t._$AP = u), t._$AQ ?? (t._$AQ = f));
|
||||
};
|
||||
class p extends A {
|
||||
constructor() {
|
||||
super(...arguments), this._$AN = void 0;
|
||||
}
|
||||
_$AT(s, e, i) {
|
||||
super._$AT(s, e, i), l(this), this.isConnected = s._$AU;
|
||||
}
|
||||
_$AO(s, e = !0) {
|
||||
var i, h;
|
||||
s !== this.isConnected && (this.isConnected = s, s ? (i = this.reconnected) == null || i.call(this) : (h = this.disconnected) == null || h.call(this)), e && (n(this, s), r(this));
|
||||
}
|
||||
setValue(s) {
|
||||
if (d(this._$Ct)) this._$Ct._$AI(s, this);
|
||||
else {
|
||||
const e = [...this._$Ct._$AH];
|
||||
e[this._$Ci] = s, this._$Ct._$AI(e, this, 0);
|
||||
}
|
||||
}
|
||||
disconnected() {
|
||||
}
|
||||
reconnected() {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const g = () => new C();
|
||||
class C {
|
||||
}
|
||||
const c = /* @__PURE__ */ new WeakMap(), M = a(class extends p {
|
||||
render(t) {
|
||||
return $;
|
||||
}
|
||||
update(t, [s]) {
|
||||
var i;
|
||||
const e = s !== this.G;
|
||||
return e && this.G !== void 0 && this.rt(void 0), (e || this.lt !== this.ct) && (this.G = s, 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 s = this.ht ?? globalThis;
|
||||
let e = c.get(s);
|
||||
e === void 0 && (e = /* @__PURE__ */ new WeakMap(), c.set(s, e)), e.get(this.G) !== void 0 && this.G.call(this.ht, void 0), e.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
|
||||
} else this.G.value = t;
|
||||
}
|
||||
get lt() {
|
||||
var t, s;
|
||||
return typeof this.G == "function" ? (t = c.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (s = this.G) == null ? void 0 : s.value;
|
||||
}
|
||||
disconnected() {
|
||||
this.lt === this.ct && this.rt(void 0);
|
||||
}
|
||||
reconnected() {
|
||||
this.rt(this.ct);
|
||||
}
|
||||
});
|
||||
export {
|
||||
g as e,
|
||||
M as n
|
||||
};
|
||||
@@ -160,12 +160,12 @@ table {
|
||||
display: grid;
|
||||
min-height: 80px;
|
||||
grid-template-areas:
|
||||
". . color-chip"
|
||||
"title comps color-chip"
|
||||
"description . color-chip"
|
||||
"subscription-url . color-chip"
|
||||
"actions . color-chip"
|
||||
". . color-chip";
|
||||
". . color-chip"
|
||||
"title comps color-chip"
|
||||
"description description color-chip"
|
||||
"subscription-url subscription-url color-chip"
|
||||
"actions actions color-chip"
|
||||
". . color-chip";
|
||||
grid-template-rows: 12px auto auto auto auto 12px;
|
||||
grid-template-columns: min-content auto 80px;
|
||||
color: inherit;
|
||||
@@ -220,6 +220,8 @@ table {
|
||||
.actions {
|
||||
grid-area: actions;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -10,12 +10,4 @@
|
||||
|
||||
<pre>{{ addressbook|json }}</pre>
|
||||
|
||||
<h2>Delete</h2>
|
||||
|
||||
<section>
|
||||
<form method="POST" action="/frontend/user/{{addressbook.principal}}/addressbook/{{addressbook.id}}/delete">
|
||||
<button type="submit">Move to trash</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -31,12 +31,4 @@
|
||||
|
||||
<pre>{{ calendar|json }}</pre>
|
||||
|
||||
<h2>Delete</h2>
|
||||
|
||||
<section>
|
||||
<form method="POST" action="/frontend/user/{{calendar.principal}}/calendar/{{calendar.id}}/delete">
|
||||
<button type="submit">Move to trash</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{%endblock %}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% block imports %}
|
||||
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
|
||||
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
|
||||
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -83,9 +84,10 @@
|
||||
<span class="subscription-url">{{ subscription_url }}</span>
|
||||
{% endif %}
|
||||
<div class="actions">
|
||||
<form action="/caldav/principal/{{ calendar.principal }}/calendar/{{ calendar.id }}" target="_blank" method="GET">
|
||||
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
@@ -114,6 +116,7 @@
|
||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
@@ -139,6 +142,7 @@
|
||||
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
<delete-button trash href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -160,6 +164,7 @@
|
||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -26,9 +26,9 @@ pub use config::FrontendConfig;
|
||||
use oidc_user_store::OidcUserStore;
|
||||
|
||||
use crate::routes::{
|
||||
addressbook::{route_addressbook, route_addressbook_restore, route_delete_addressbook},
|
||||
addressbook::{route_addressbook, route_addressbook_restore},
|
||||
app_token::{route_delete_app_token, route_post_app_token},
|
||||
calendar::{route_calendar, route_calendar_restore, route_delete_calendar},
|
||||
calendar::{route_calendar, route_calendar_restore},
|
||||
login::{route_get_login, route_post_login, route_post_logout},
|
||||
user::{route_get_home, route_root, route_user_named},
|
||||
};
|
||||
@@ -60,10 +60,6 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
||||
"/user/{user}/calendar/{calendar}",
|
||||
get(route_calendar::<CS>),
|
||||
)
|
||||
.route(
|
||||
"/user/{user}/calendar/{calendar}/delete",
|
||||
post(route_delete_calendar::<CS>),
|
||||
)
|
||||
.route(
|
||||
"/user/{user}/calendar/{calendar}/restore",
|
||||
post(route_calendar_restore::<CS>),
|
||||
@@ -73,10 +69,6 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
||||
"/user/{user}/addressbook/{addressbook}",
|
||||
get(route_addressbook::<AS>),
|
||||
)
|
||||
.route(
|
||||
"/user/{user}/addressbook/{addressbook}/delete",
|
||||
post(route_delete_addressbook::<AS>),
|
||||
)
|
||||
.route(
|
||||
"/user/{user}/addressbook/{addressbook}/restore",
|
||||
post(route_addressbook_restore::<AS>),
|
||||
|
||||
@@ -13,7 +13,7 @@ use axum_extra::{TypedHeader, extract::Host};
|
||||
use chrono::{Duration, Utc};
|
||||
use headers::UserAgent;
|
||||
use http::StatusCode;
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
@@ -101,7 +101,7 @@ struct NextcloudLoginPage {
|
||||
pub(crate) async fn get_nextcloud_flow(
|
||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||
Path(flow_id): Path<String>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if let Some(flow) = state.flows.read().await.get(&flow_id) {
|
||||
Ok(Html(
|
||||
@@ -131,7 +131,7 @@ struct NextcloudLoginSuccessPage {
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub(crate) async fn post_nextcloud_flow(
|
||||
user: User,
|
||||
user: Principal,
|
||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||
Path(flow_id): Path<String>,
|
||||
Host(host): Host,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rustical_oidc::UserStore;
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal};
|
||||
|
||||
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> {
|
||||
self.0
|
||||
.insert_principal(
|
||||
User {
|
||||
Principal {
|
||||
id: id.to_owned(),
|
||||
displayname: None,
|
||||
principal_type: Default::default(),
|
||||
|
||||
@@ -10,7 +10,7 @@ use axum::{
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::Referer;
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Addressbook, AddressbookStore, auth::User};
|
||||
use rustical_store::{Addressbook, AddressbookStore, auth::Principal};
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/addressbook.html")]
|
||||
@@ -21,7 +21,7 @@ struct AddressbookPage {
|
||||
pub async fn route_addressbook<AS: AddressbookStore>(
|
||||
Path((owner, addrbook_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<AS>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
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>(
|
||||
Path((owner, addressbook_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<AS>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
referer: Option<TypedHeader<Referer>>,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
@@ -47,19 +47,3 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
|
||||
None => (StatusCode::CREATED, "Restored").into_response(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn route_delete_addressbook<AS: AddressbookStore>(
|
||||
Path((owner, addressbook_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<AS>>,
|
||||
user: User,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
||||
}
|
||||
|
||||
store
|
||||
.delete_addressbook(&owner, &addressbook_id, true)
|
||||
.await?;
|
||||
|
||||
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use headers::{ContentType, HeaderMapExt};
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||
use rand::{Rng, distr::Alphanumeric};
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(crate) struct PostAppTokenForm {
|
||||
}
|
||||
|
||||
pub async fn route_post_app_token<AP: AuthenticationProvider>(
|
||||
user: User,
|
||||
user: Principal,
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
Path(user_id): Path<String>,
|
||||
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>(
|
||||
user: User,
|
||||
user: Principal,
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
Path((user_id, token_id)): Path<(String, String)>,
|
||||
) -> Result<Redirect, rustical_store::Error> {
|
||||
|
||||
@@ -10,7 +10,7 @@ use axum::{
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::Referer;
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Calendar, CalendarStore, auth::User};
|
||||
use rustical_store::{Calendar, CalendarStore, auth::Principal};
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/calendar.html")]
|
||||
@@ -21,7 +21,7 @@ struct CalendarPage {
|
||||
pub async fn route_calendar<C: CalendarStore>(
|
||||
Path((owner, cal_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<C>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
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>(
|
||||
Path((owner, cal_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<CS>>,
|
||||
user: User,
|
||||
user: Principal,
|
||||
referer: Option<TypedHeader<Referer>>,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
@@ -47,17 +47,3 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
|
||||
None => (StatusCode::CREATED, "Restored").into_response(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn route_delete_calendar<C: CalendarStore>(
|
||||
Path((owner, cal_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<C>>,
|
||||
user: User,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
||||
}
|
||||
|
||||
store.delete_calendar(&owner, &cal_id, true).await?;
|
||||
|
||||
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ use headers::UserAgent;
|
||||
use http::StatusCode;
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||
auth::{AuthenticationProvider, User, user::AppToken},
|
||||
auth::{AppToken, AuthenticationProvider, Principal},
|
||||
};
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/user.html")]
|
||||
pub struct UserPage {
|
||||
pub user: User,
|
||||
pub user: Principal,
|
||||
pub app_tokens: Vec<AppToken>,
|
||||
pub calendars: Vec<Calendar>,
|
||||
pub deleted_calendars: Vec<Calendar>,
|
||||
@@ -39,7 +39,7 @@ pub async fn route_user_named<
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
TypedHeader(user_agent): TypedHeader<UserAgent>,
|
||||
Host(host): Host,
|
||||
user: User,
|
||||
user: Principal,
|
||||
) -> impl IntoResponse {
|
||||
if user_id != user.id {
|
||||
return StatusCode::UNAUTHORIZED.into_response();
|
||||
@@ -81,11 +81,11 @@ pub async fn route_user_named<
|
||||
.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))
|
||||
}
|
||||
|
||||
pub async fn route_root(user: Option<User>) -> Redirect {
|
||||
pub async fn route_root(user: Option<Principal>) -> Redirect {
|
||||
match user {
|
||||
Some(user) => route_get_home(user).await,
|
||||
None => Redirect::to("/frontend/login"),
|
||||
|
||||
@@ -138,7 +138,8 @@ pub async fn route_post_oidc(
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AuthCallbackQuery {
|
||||
code: AuthorizationCode,
|
||||
iss: IssuerUrl,
|
||||
// RFC 9207
|
||||
iss: Option<IssuerUrl>,
|
||||
state: String,
|
||||
}
|
||||
|
||||
@@ -153,7 +154,9 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
|
||||
) -> Result<Response, OidcError> {
|
||||
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
|
||||
.remove::<OidcState>(SESSION_KEY_OIDC_STATE)
|
||||
.await?
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
pub mod middleware;
|
||||
pub mod user;
|
||||
mod principal;
|
||||
use crate::error::Error;
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub use principal::{AppToken, Principal, PrincipalType};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AuthenticationProvider: Send + Sync + 'static {
|
||||
async fn get_principals(&self) -> Result<Vec<User>, crate::Error>;
|
||||
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>;
|
||||
async fn get_principals(&self) -> Result<Vec<Principal>, 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 insert_principal(&self, user: User, overwrite: bool) -> Result<(), crate::Error>;
|
||||
async fn validate_password(&self, user_id: &str, password: &str)
|
||||
-> Result<Option<User>, Error>;
|
||||
async fn validate_app_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error>;
|
||||
async fn insert_principal(&self, user: Principal, overwrite: bool) -> Result<(), crate::Error>;
|
||||
async fn validate_password(
|
||||
&self,
|
||||
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
|
||||
async fn add_app_token(
|
||||
&self,
|
||||
@@ -28,5 +37,3 @@ pub trait AuthenticationProvider: Send + Sync + 'static {
|
||||
}
|
||||
|
||||
pub use middleware::AuthenticationMiddleware;
|
||||
use user::AppToken;
|
||||
pub use user::User;
|
||||
|
||||
@@ -78,8 +78,7 @@ pub struct AppToken {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
// TODO: Rename this to Principal
|
||||
pub struct User {
|
||||
pub struct Principal {
|
||||
pub id: String,
|
||||
pub displayname: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -89,7 +88,7 @@ pub struct User {
|
||||
pub memberships: Vec<String>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
impl Principal {
|
||||
/// Returns true if the user is either
|
||||
/// - the principal itself
|
||||
/// - 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 {
|
||||
&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;
|
||||
|
||||
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;
|
||||
|
||||
async fn from_request_parts(
|
||||
@@ -58,6 +58,7 @@ pub trait CalendarStore: Send + Sync + 'static {
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
object_id: &str,
|
||||
show_deleted: bool,
|
||||
) -> Result<CalendarObject, Error>;
|
||||
async fn put_object(
|
||||
&self,
|
||||
|
||||
@@ -80,11 +80,13 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
||||
Err(Error::ReadOnly)
|
||||
} else {
|
||||
self.birthday_store
|
||||
.delete_object(principal, cal_id, object_id, use_trashbin)
|
||||
.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,
|
||||
cal_id: &str,
|
||||
object_id: &str,
|
||||
show_deleted: bool,
|
||||
) -> Result<CalendarObject, Error> {
|
||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
||||
self.birthday_store
|
||||
.get_object(principal, cal_id, object_id)
|
||||
.get_object(principal, cal_id, object_id, show_deleted)
|
||||
.await
|
||||
} else {
|
||||
self.cal_store
|
||||
.get_object(principal, cal_id, object_id)
|
||||
.get_object(principal, cal_id, object_id, show_deleted)
|
||||
.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,
|
||||
cal_id: &str,
|
||||
object_id: &str,
|
||||
show_deleted: bool,
|
||||
) -> Result<CalendarObject, Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
let (addressobject_id, date_type) = object_id.rsplit_once("-").ok_or(Error::NotFound)?;
|
||||
self.0
|
||||
.get_object(principal, cal_id, addressobject_id, false)
|
||||
.get_object(principal, cal_id, addressobject_id, show_deleted)
|
||||
.await?
|
||||
.get_significant_dates()?
|
||||
.remove(date_type)
|
||||
|
||||
@@ -250,7 +250,7 @@ impl SqliteAddressbookStore {
|
||||
) -> Result<AddressObject, rustical_store::Error> {
|
||||
Ok(sqlx::query_as!(
|
||||
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,
|
||||
addressbook_id,
|
||||
object_id,
|
||||
|
||||
@@ -296,13 +296,15 @@ impl SqliteCalendarStore {
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
object_id: &str,
|
||||
show_deleted: bool,
|
||||
) -> Result<CalendarObject, Error> {
|
||||
sqlx::query_as!(
|
||||
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,
|
||||
cal_id,
|
||||
object_id
|
||||
object_id,
|
||||
show_deleted
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
@@ -454,7 +456,7 @@ impl SqliteCalendarStore {
|
||||
.unwrap_or(0);
|
||||
|
||||
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),
|
||||
Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id),
|
||||
Err(err) => return Err(err),
|
||||
@@ -557,8 +559,9 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
object_id: &str,
|
||||
show_deleted: bool,
|
||||
) -> 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]
|
||||
|
||||
@@ -7,7 +7,7 @@ use pbkdf2::{
|
||||
};
|
||||
use rustical_store::{
|
||||
Error, Secret,
|
||||
auth::{AuthenticationProvider, User, user::AppToken},
|
||||
auth::{AppToken, AuthenticationProvider, Principal},
|
||||
};
|
||||
use sqlx::{SqlitePool, types::Json};
|
||||
use tracing::instrument;
|
||||
@@ -21,11 +21,11 @@ struct PrincipalRow {
|
||||
memberships: Option<Json<Vec<Option<String>>>>,
|
||||
}
|
||||
|
||||
impl TryFrom<PrincipalRow> for User {
|
||||
impl TryFrom<PrincipalRow> for Principal {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: PrincipalRow) -> Result<Self, Self::Error> {
|
||||
Ok(User {
|
||||
Ok(Principal {
|
||||
id: value.id,
|
||||
displayname: value.displayname,
|
||||
password: value.password_hash.map(Secret::from),
|
||||
@@ -49,8 +49,8 @@ pub struct SqlitePrincipalStore {
|
||||
#[async_trait]
|
||||
impl AuthenticationProvider for SqlitePrincipalStore {
|
||||
#[instrument]
|
||||
async fn get_principals(&self) -> Result<Vec<User>, Error> {
|
||||
let result: Result<Vec<User>, Error> = sqlx::query_as!(
|
||||
async fn get_principals(&self) -> Result<Vec<Principal>, Error> {
|
||||
let result: Result<Vec<Principal>, Error> = sqlx::query_as!(
|
||||
PrincipalRow,
|
||||
r#"
|
||||
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
|
||||
.map_err(crate::Error::from)?
|
||||
.into_iter()
|
||||
.map(User::try_from)
|
||||
.map(Principal::try_from)
|
||||
.collect();
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
#[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!(
|
||||
PrincipalRow,
|
||||
r#"
|
||||
@@ -83,7 +83,7 @@ impl AuthenticationProvider for SqlitePrincipalStore {
|
||||
.fetch_optional(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?
|
||||
.map(User::try_from);
|
||||
.map(Principal::try_from);
|
||||
if let Some(row) = row {
|
||||
Ok(Some(row?))
|
||||
} else {
|
||||
@@ -103,7 +103,7 @@ impl AuthenticationProvider for SqlitePrincipalStore {
|
||||
#[instrument]
|
||||
async fn insert_principal(
|
||||
&self,
|
||||
user: User,
|
||||
user: Principal,
|
||||
overwrite: bool,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
// 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))]
|
||||
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? {
|
||||
if password_auth::verify_password(token, app_token.token.as_ref()).is_ok() {
|
||||
return self.get_principal(user_id).await;
|
||||
@@ -169,8 +173,8 @@ impl AuthenticationProvider for SqlitePrincipalStore {
|
||||
&self,
|
||||
user_id: &str,
|
||||
password_input: &str,
|
||||
) -> Result<Option<User>, Error> {
|
||||
let user: User = match self.get_principal(user_id).await? {
|
||||
) -> Result<Option<Principal>, Error> {
|
||||
let user: Principal = match self.get_principal(user_id).await? {
|
||||
Some(user) => user,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@ a CalDAV/CardDAV server
|
||||
|
||||
!!! warning
|
||||
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. :)
|
||||
|
||||
## Features
|
||||
@@ -25,3 +26,4 @@ If you still want to play around with it in its current state, absolutely feel f
|
||||
- GNOME Accounts, GNOME Calendar, GNOME Contacts
|
||||
- Evolution
|
||||
- Apple Calendar
|
||||
- Home Assistant integration
|
||||
|
||||
@@ -6,7 +6,7 @@ use figment::{
|
||||
providers::{Env, Format, Toml},
|
||||
};
|
||||
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)]
|
||||
pub struct PrincipalsArgs {
|
||||
@@ -99,7 +99,7 @@ pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> {
|
||||
};
|
||||
principal_store
|
||||
.insert_principal(
|
||||
User {
|
||||
Principal {
|
||||
id,
|
||||
displayname: name,
|
||||
principal_type: principal_type.unwrap_or_default(),
|
||||
|
||||
Reference in New Issue
Block a user