Compare commits

..

12 Commits

Author SHA1 Message Date
Lennart
668fa86e3c Update version to 0.3.3 2025-06-22 21:46:37 +02:00
Lennart
23d2024644 Update note on production-readiness 2025-06-22 19:43:46 +02:00
Lennart
15aadcf1be Rename User struct to Principal 2025-06-19 20:59:59 +02:00
Lennart
4a3b7d7ce6 Update typescript config 2025-06-19 20:52:17 +02:00
Lennart
1a2f3b8f8a frontend: Move collection creation to dialog 2025-06-18 18:09:19 +02:00
Lennart
9e8c218308 Remove unused p256 dependency 2025-06-18 17:49:00 +02:00
Lennart
f2adce739b Update version to v0.3.2 2025-06-15 17:12:34 +02:00
Lennart
0415664ff3 calendar_store: Fix deleted objects being returned 2025-06-15 16:31:07 +02:00
Lennart
677e0082fa multistatus response: Set No-Cache 2025-06-15 13:16:37 +02:00
Lennart
a387885b0a Remove calendar-proxy-write from caldav principal 2025-06-15 11:44:44 +02:00
Lennart
990b953055 Fix typo on store preventing us from deleting calendar objects 2025-06-15 10:37:51 +02:00
Lennart
36b47a645d Fix missing ece backend, finally managed to statically link openssl 2025-06-14 22:26:01 +02:00
63 changed files with 535 additions and 298 deletions

View File

@@ -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"
}

View File

@@ -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
View File

@@ -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.3"
dependencies = [
"anyhow",
"argon2",
@@ -2712,7 +2779,7 @@ dependencies = [
[[package]]
name = "rustical_caldav"
version = "0.2.2"
version = "0.3.3"
dependencies = [
"async-trait",
"axum",
@@ -2747,7 +2814,7 @@ dependencies = [
[[package]]
name = "rustical_carddav"
version = "0.2.2"
version = "0.3.3"
dependencies = [
"async-trait",
"axum",
@@ -2779,7 +2846,7 @@ dependencies = [
[[package]]
name = "rustical_dav"
version = "0.2.2"
version = "0.3.3"
dependencies = [
"async-trait",
"axum",
@@ -2804,7 +2871,7 @@ dependencies = [
[[package]]
name = "rustical_dav_push"
version = "0.2.2"
version = "0.3.3"
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.3"
dependencies = [
"askama",
"askama_web",
@@ -2863,7 +2930,7 @@ dependencies = [
[[package]]
name = "rustical_ical"
version = "0.2.2"
version = "0.3.3"
dependencies = [
"axum",
"chrono",
@@ -2881,7 +2948,7 @@ dependencies = [
[[package]]
name = "rustical_oidc"
version = "0.2.2"
version = "0.3.3"
dependencies = [
"async-trait",
"axum",
@@ -2896,7 +2963,7 @@ dependencies = [
[[package]]
name = "rustical_store"
version = "0.2.2"
version = "0.3.3"
dependencies = [
"anyhow",
"async-trait",
@@ -2930,7 +2997,7 @@ dependencies = [
[[package]]
name = "rustical_store_sqlite"
version = "0.2.2"
version = "0.3.3"
dependencies = [
"async-trait",
"chrono",
@@ -2950,7 +3017,7 @@ dependencies = [
[[package]]
name = "rustical_xml"
version = "0.2.2"
version = "0.3.3"
dependencies = [
"quick-xml",
"thiserror 2.0.12",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"]
[workspace.package]
version = "0.3.0"
version = "0.3.3"
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 }

View File

@@ -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"

View File

@@ -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

View File

@@ -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);

View File

@@ -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,

View File

@@ -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> {

View File

@@ -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()),

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,7 +291,7 @@ impl Resource for CalendarResource {
Some(&self.cal.principal)
}
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> {
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
if self.cal.subscription_url.is_some() || self.read_only {
return Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.cal.principal),

View File

@@ -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";

View File

@@ -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,

View File

@@ -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),
))

View File

@@ -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,

View File

@@ -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))),

View File

@@ -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),
))

View File

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

View File

@@ -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";

View File

@@ -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,

View File

@@ -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),
))

View File

@@ -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";

View File

@@ -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);

View File

@@ -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> {

View File

@@ -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> {

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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>>,

View File

@@ -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,

View File

@@ -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),
))

View File

@@ -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";

View File

@@ -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",

View File

@@ -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),
))

View File

@@ -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";

View File

@@ -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()
}
}

View File

@@ -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

View File

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

View File

@@ -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>
`
}

View File

@@ -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) {
@@ -89,7 +95,7 @@ export class CreateCalendarForm extends LitElement {
alert("No calendar components selected")
return
}
await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
await this.client.createDirectory(`/ principal / ${this.user}/${this.id}`, {
data: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>

View File

@@ -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"

View File

@@ -1,45 +1,49 @@
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 { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
import { a as b } from "./webdav-Bz4I5vNH.mjs";
var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, a = (t, s, l, r) => {
for (var e = r > 1 ? void 0 : r ? f(s, l) : s, n = t.length - 1, p; n >= 0; n--)
(p = t[n]) && (e = (r ? p(s, l, e) : p(e)) || e);
return r && e && y(s, l, e), e;
};
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;
}
@@ -61,20 +65,20 @@ let i = class extends d {
}), window.location.reload(), null;
}
};
r([
n()
a([
o()
], i.prototype, "user", 2);
r([
n()
a([
o()
], i.prototype, "id", 2);
r([
n()
a([
o()
], i.prototype, "displayname", 2);
r([
n()
a([
o()
], i.prototype, "description", 2);
i = r([
c("create-addressbook-form")
i = a([
h("create-addressbook-form")
], i);
export {
i as CreateAddressbookForm

View File

@@ -1,62 +1,66 @@
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 { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
import { a as b } from "./webdav-Bz4I5vNH.mjs";
var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, l, s) => {
for (var i = s > 1 ? void 0 : s ? $(e, l) : e, n = t.length - 1, p; n >= 0; n--)
(p = t[n]) && (i = (s ? p(e, l, i) : p(i)) || i);
return s && i && y(e, l, i), i;
};
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 = d(), this.form = d();
}
createRenderRoot() {
return this;
}
render() {
return c`
<section>
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${m(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit}>
<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>
<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;
}
@@ -68,7 +72,7 @@ let r = class extends m {
alert("No calendar components selected");
return;
}
return await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
return await this.client.createDirectory(`/ principal / ${this.user}/${this.id}`, {
data: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>
@@ -78,7 +82,7 @@ let r = class extends m {
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""}
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</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 +93,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

View File

@@ -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,

View File

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

View File

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

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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) {
@@ -51,7 +51,7 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
pub async fn route_delete_addressbook<AS: AddressbookStore>(
Path((owner, addressbook_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());

View File

@@ -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> {

View File

@@ -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) {
@@ -51,7 +51,7 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
pub async fn route_delete_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());

View File

@@ -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"),

View File

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

View File

@@ -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(

View File

@@ -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,

View File

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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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]

View File

@@ -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),
};

View File

@@ -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

View File

@@ -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(),