Compare commits

..

19 Commits

Author SHA1 Message Date
Lennart
c646986c56 Version 0.4.1 2025-06-23 14:08:06 +02:00
Lennart
503cbe3699 fix: Add default frontend config 2025-06-23 14:07:38 +02:00
Lennart
79c66a0b46 fix(caldav): Fix permissions to allow for deletion of calendar subscriptions
fixes #84
2025-06-23 14:04:09 +02:00
Lennart
e5687c6e43 fix(frontend): calendar subscription creation 2025-06-23 14:03:10 +02:00
Lennart
79b67a17c3 Implement deletion button to permanently delete collections 2025-06-23 13:48:00 +02:00
Lennart
7d18faff69 version 0.3.6 2025-06-23 11:21:04 +02:00
Lennart
753f8e90d3 fix(frontend): Fix calendar download link 2025-06-23 11:20:44 +02:00
Lennart
701fa9dd9c Version 3.4.5 2025-06-23 08:54:26 +02:00
Lennart
31b17cfe7f Frontend: Fix dumb typo in calendar creation form
Fixes #82
2025-06-23 08:53:50 +02:00
Lennart
d802a0085a Add Home Assistant to tested clients 2025-06-23 00:42:45 +02:00
Lennart
786b15f5b9 version 0.3.4 2025-06-22 23:58:49 +02:00
Lennart
f5d097ac55 oidc: Fix for OIDC servers not supporting RFC 9207
see #81
2025-06-22 23:55:57 +02:00
Lennart
668fa86e3c Update version to 0.3.3 2025-06-22 21:46:37 +02:00
Lennart
23d2024644 Update note on production-readiness 2025-06-22 19:43:46 +02:00
Lennart
15aadcf1be Rename User struct to Principal 2025-06-19 20:59:59 +02:00
Lennart
4a3b7d7ce6 Update typescript config 2025-06-19 20:52:17 +02:00
Lennart
1a2f3b8f8a frontend: Move collection creation to dialog 2025-06-18 18:09:19 +02:00
Lennart
9e8c218308 Remove unused p256 dependency 2025-06-18 17:49:00 +02:00
Lennart
f2adce739b Update version to v0.3.2 2025-06-15 17:12:34 +02:00
66 changed files with 538 additions and 283 deletions

23
Cargo.lock generated
View File

@@ -2736,7 +2736,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -2779,7 +2779,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2814,7 +2814,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2846,7 +2846,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2871,7 +2871,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2883,7 +2883,6 @@ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"log", "log",
"openssl", "openssl",
"p256",
"quick-xml", "quick-xml",
"rand 0.9.1", "rand 0.9.1",
"reqwest", "reqwest",
@@ -2898,7 +2897,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -2931,7 +2930,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -2949,7 +2948,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2964,7 +2963,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -2998,7 +2997,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3018,7 +3017,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.3.0" version = "0.4.1"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.12", "thiserror 2.0.12",

View File

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

View File

@@ -4,8 +4,8 @@ a CalDAV/CardDAV server
> [!WARNING] > [!WARNING]
RustiCal is **not production-ready!** RustiCal is **not production-ready!**
While I've started migrating to RustiCal and becoming more confident, I've been using RustiCal for the last few weeks and I'm slowly becoming more confident,
please know that bugs and rough edges will still occur. however you'd be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
## Features ## Features
@@ -30,3 +30,4 @@ a CalDAV/CardDAV server
- GNOME Accounts, GNOME Calendar, GNOME Contacts - GNOME Accounts, GNOME Calendar, GNOME Contacts
- Evolution - Evolution
- Apple Calendar - Apple Calendar
- Home Assistant integration

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ use axum::handler::Handler;
use axum::response::Response; use axum::response::Response;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{CalendarStore, SubscriptionStore}; use rustical_store::{CalendarStore, SubscriptionStore};
use std::convert::Infallible; use std::convert::Infallible;
use std::sync::Arc; use std::sync::Arc;
@@ -48,7 +48,7 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
type PathComponents = (String, String); // principal, calendar_id type PathComponents = (String, String); // principal, calendar_id
type Resource = CalendarResource; type Resource = CalendarResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy, webdav-push"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy, webdav-push";

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ use async_trait::async_trait;
use axum::{extract::Request, handler::Handler, response::Response}; use axum::{extract::Request, handler::Handler, response::Response};
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::{CalendarStore, auth::User}; use rustical_store::{CalendarStore, auth::Principal};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::{convert::Infallible, sync::Arc}; use std::{convert::Infallible, sync::Arc};
use tower::Service; use tower::Service;
@@ -46,7 +46,7 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
type Resource = CalendarObjectResource; type Resource = CalendarObjectResource;
type MemberType = CalendarObjectResource; type MemberType = CalendarObjectResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access";

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ use crate::{CalDavPrincipalUri, Error};
use async_trait::async_trait; use async_trait::async_trait;
use axum::Router; use axum::Router;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use rustical_store::{CalendarStore, SubscriptionStore}; use rustical_store::{CalendarStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
@@ -40,7 +40,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
type MemberType = CalendarResource; type MemberType = CalendarResource;
type Resource = PrincipalResource; type Resource = PrincipalResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ use axum::{extract::Request, handler::Handler, response::Response};
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::{AddressbookStore, auth::User}; use rustical_store::{AddressbookStore, auth::Principal};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::{convert::Infallible, sync::Arc}; use std::{convert::Infallible, sync::Arc};
use tower::Service; use tower::Service;
@@ -37,7 +37,7 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
type Resource = AddressObjectResource; type Resource = AddressObjectResource;
type MemberType = AddressObjectResource; type MemberType = AddressObjectResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook"; const DAV_HEADER: &str = "1, 3, access-control, addressbook";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ use axum::handler::Handler;
use axum::response::Response; use axum::response::Response;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{AddressbookStore, SubscriptionStore}; use rustical_store::{AddressbookStore, SubscriptionStore};
use std::convert::Infallible; use std::convert::Infallible;
use std::sync::Arc; use std::sync::Arc;
@@ -51,7 +51,7 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
type PathComponents = (String, String); // principal, addressbook_id type PathComponents = (String, String); // principal, addressbook_id
type Resource = AddressbookResource; type Resource = AddressbookResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook, webdav-push"; const DAV_HEADER: &str = "1, 3, access-control, addressbook, webdav-push";

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ use crate::{CardDavPrincipalUri, Error};
use async_trait::async_trait; use async_trait::async_trait;
use axum::Router; use axum::Router;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use rustical_store::{AddressbookStore, SubscriptionStore}; use rustical_store::{AddressbookStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
@@ -51,7 +51,7 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
type MemberType = AddressbookResource; type MemberType = AddressbookResource;
type Resource = PrincipalResource; type Resource = PrincipalResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook"; const DAV_HEADER: &str = "1, 3, access-control, addressbook";

View File

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

View File

@@ -47,8 +47,9 @@ pub async fn route_delete<R: ResourceService>(
) -> Result<(), R::Error> { ) -> Result<(), R::Error> {
let resource = resource_service.get_resource(path_components).await?; 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)?; let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Write) { if !privileges.has(&UserPrivilege::WriteProperties) {
return Err(Error::Unauthorized.into()); return Err(Error::Unauthorized.into());
} }

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav"; import { createClient } from "webdav";
@customElement("create-addressbook-form") @customElement("create-addressbook-form")
@@ -24,12 +25,15 @@ export class CreateAddressbookForm extends LitElement {
@property() @property()
description: String = '' description: String = ''
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
override render() { override render() {
return html` return html`
<section> <button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
<h3>Create calendar</h3> <dialog ${ref(this.dialog)}>
<form @submit=${this.submit}> <h3>Create addressbook</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
id id
<input type="text" name="id" @change=${e => this.id = e.target.value} /> <input type="text" name="id" @change=${e => this.id = e.target.value} />
@@ -46,8 +50,9 @@ export class CreateAddressbookForm extends LitElement {
</label> </label>
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
</form> </form>
</section> </dialog>
` `
} }

View File

@@ -1,5 +1,6 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav"; import { createClient } from "webdav";
@customElement("create-calendar-form") @customElement("create-calendar-form")
@@ -30,12 +31,16 @@ export class CreateCalendarForm extends LitElement {
@property() @property()
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set() components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
override render() { override render() {
return html` return html`
<section> <button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${ref(this.dialog)}>
<h3>Create calendar</h3> <h3>Create calendar</h3>
<form @submit=${this.submit}> <form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
id id
<input type="text" name="id" @change=${e => this.id = e.target.value} /> <input type="text" name="id" @change=${e => this.id = e.target.value} />
@@ -69,9 +74,10 @@ export class CreateCalendarForm extends LitElement {
`)} `)}
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
</form> <button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
</section> </form>
` </dialog>
`
} }
async submit(e: SubmitEvent) { async submit(e: SubmitEvent) {
@@ -97,7 +103,7 @@ export class CreateCalendarForm extends LitElement {
<displayname>${this.displayname}</displayname> <displayname>${this.displayname}</displayname>
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''} ${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''} ${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''}
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ''} ${this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ''}
<CAL:supported-calendar-component-set> <CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')} ${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')}
</CAL:supported-calendar-component-set> </CAL:supported-calendar-component-set>

View File

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

View File

@@ -1,10 +1,13 @@
{ {
"module": "nodenext", "module": "nodenext",
"moduleResolution": "nodenext",
"compilerOptions": { "compilerOptions": {
"target": "es2020", "target": "es2024",
"experimentalDecorators": true, "experimentalDecorators": true,
"useDefineForClassFields": false "useDefineForClassFields": false,
"lib": [
"dom",
"es2024"
]
}, },
"include": [ "include": [
"lib/**/*.ts" "lib/**/*.ts"

View File

@@ -15,6 +15,7 @@ export default defineConfig({
input: [ input: [
"lib/create-calendar-form.ts", "lib/create-calendar-form.ts",
"lib/create-addressbook-form.ts", "lib/create-addressbook-form.ts",
"lib/delete-button.ts",
], ],
output: { output: {
dir: "../public/assets/js/", dir: "../public/assets/js/",

View File

@@ -1,45 +1,50 @@
import { i as d, x as m } from "./lit-Dq9MfRDi.mjs"; import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
import { n, t as c } from "./property-DwhV4xIV.mjs"; import { n as o, t as h } from "./property-DYFkTqgI.mjs";
import { a as u } from "./webdav-Bz4I5vNH.mjs"; import { e as d, n as m } from "./ref-nf9JiOyl.mjs";
var h = Object.defineProperty, y = Object.getOwnPropertyDescriptor, r = (e, a, o, s) => { import { a as b } from "./webdav-Bz4I5vNH.mjs";
for (var t = s > 1 ? void 0 : s ? y(a, o) : a, p = e.length - 1, l; p >= 0; p--) var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, r = (t, a, n, s) => {
(l = e[p]) && (t = (s ? l(a, o, t) : l(t)) || t); for (var e = s > 1 ? void 0 : s ? f(a, n) : a, l = t.length - 1, p; l >= 0; l--)
return s && t && h(a, o, t), t; (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() { constructor() {
super(), this.client = u("/carddav"), this.user = "", this.id = "", this.displayname = "", this.description = ""; super(), this.client = b("/carddav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.dialog = d(), this.form = d();
} }
createRenderRoot() { createRenderRoot() {
return this; return this;
} }
render() { render() {
return m` return u`
<section> <button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
<h3>Create calendar</h3> <dialog ${m(this.dialog)}>
<form @submit=${this.submit}> <h3>Create addressbook</h3>
<form @submit=${this.submit} ${m(this.form)}>
<label> <label>
id id
<input type="text" name="id" @change=${(e) => this.id = e.target.value} /> <input type="text" name="id" @change=${(t) => this.id = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Displayname Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(e) => this.displayname = e.target.value} /> <input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Description Description
<input type="text" name="description" @change=${(e) => this.description = e.target.value} /> <input type="text" name="description" @change=${(t) => this.description = t.target.value} />
</label> </label>
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
<button type="submit" @click=${(t) => {
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
}}> Cancel </button>
</form> </form>
</section> </dialog>
`; `;
} }
async submit(e) { async submit(t) {
if (console.log(this.displayname), e.preventDefault(), !this.id) { if (console.log(this.displayname), t.preventDefault(), !this.id) {
alert("Empty id"); alert("Empty id");
return; return;
} }
@@ -62,19 +67,19 @@ let i = class extends d {
} }
}; };
r([ r([
n() o()
], i.prototype, "user", 2); ], i.prototype, "user", 2);
r([ r([
n() o()
], i.prototype, "id", 2); ], i.prototype, "id", 2);
r([ r([
n() o()
], i.prototype, "displayname", 2); ], i.prototype, "displayname", 2);
r([ r([
n() o()
], i.prototype, "description", 2); ], i.prototype, "description", 2);
i = r([ i = r([
c("create-addressbook-form") h("create-addressbook-form")
], i); ], i);
export { export {
i as CreateAddressbookForm i as CreateAddressbookForm

View File

@@ -1,62 +1,67 @@
import { i as m, x as c } from "./lit-Dq9MfRDi.mjs"; import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
import { n as s, t as d } from "./property-DwhV4xIV.mjs"; import { n as o, t as h } from "./property-DYFkTqgI.mjs";
import { a as u } from "./webdav-Bz4I5vNH.mjs"; import { e as m, n as d } from "./ref-nf9JiOyl.mjs";
var h = Object.defineProperty, b = Object.getOwnPropertyDescriptor, a = (e, t, o, n) => { import { a as b } from "./webdav-Bz4I5vNH.mjs";
for (var i = n > 1 ? void 0 : n ? b(t, o) : t, l = e.length - 1, p; l >= 0; l--) var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, n, s) => {
(p = e[l]) && (i = (n ? p(t, o, i) : p(i)) || i); for (var i = s > 1 ? void 0 : s ? $(e, n) : e, l = t.length - 1, p; l >= 0; l--)
return n && i && h(t, o, i), i; (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() { 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() { createRenderRoot() {
return this; return this;
} }
render() { render() {
return c` return c`
<section> <button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${d(this.dialog)}>
<h3>Create calendar</h3> <h3>Create calendar</h3>
<form @submit=${this.submit}> <form @submit=${this.submit} ${d(this.form)}>
<label> <label>
id id
<input type="text" name="id" @change=${(e) => this.id = e.target.value} /> <input type="text" name="id" @change=${(t) => this.id = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Displayname Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(e) => this.displayname = e.target.value} /> <input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Description Description
<input type="text" name="description" @change=${(e) => this.description = e.target.value} /> <input type="text" name="description" @change=${(t) => this.description = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Color Color
<input type="color" name="color" @change=${(e) => this.color = e.target.value} /> <input type="color" name="color" @change=${(t) => this.color = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Subscription URL Subscription URL
<input type="text" name="subscription_url" @change=${(e) => this.subscriptionUrl = e.target.value} /> <input type="text" name="subscription_url" @change=${(t) => this.subscriptionUrl = t.target.value} />
</label> </label>
<br> <br>
${["VEVENT", "VTODO", "VJOURNAL"].map((e) => c` ${["VEVENT", "VTODO", "VJOURNAL"].map((t) => c`
<label> <label>
Support ${e} Support ${t}
<input type="checkbox" value=${e} @change=${(t) => t.target.checked ? this.components.add(t.target.value) : this.components.delete(t.target.value)} /> <input type="checkbox" value=${t} @change=${(e) => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
</label> </label>
`)} `)}
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
</form> <button type="submit" @click=${(t) => {
</section> t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
`; }}> Cancel </button>
</form>
</dialog>
`;
} }
async submit(e) { async submit(t) {
if (console.log(this.displayname), e.preventDefault(), !this.id) { if (console.log(this.displayname), t.preventDefault(), !this.id) {
alert("Empty id"); alert("Empty id");
return; return;
} }
@@ -76,9 +81,9 @@ let r = class extends m {
<displayname>${this.displayname}</displayname> <displayname>${this.displayname}</displayname>
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ""} ${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""} ${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""}
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ""} ${this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ""}
<CAL:supported-calendar-component-set> <CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map((t) => `<CAL:comp name="${t}" />`).join(` ${Array.from(this.components.keys()).map((e) => `<CAL:comp name="${e}" />`).join(`
`)} `)}
</CAL:supported-calendar-component-set> </CAL:supported-calendar-component-set>
</prop> </prop>
@@ -89,28 +94,28 @@ let r = class extends m {
} }
}; };
a([ a([
s() o()
], r.prototype, "user", 2); ], r.prototype, "user", 2);
a([ a([
s() o()
], r.prototype, "id", 2); ], r.prototype, "id", 2);
a([ a([
s() o()
], r.prototype, "displayname", 2); ], r.prototype, "displayname", 2);
a([ a([
s() o()
], r.prototype, "description", 2); ], r.prototype, "description", 2);
a([ a([
s() o()
], r.prototype, "color", 2); ], r.prototype, "color", 2);
a([ a([
s() o()
], r.prototype, "subscriptionUrl", 2); ], r.prototype, "subscriptionUrl", 2);
a([ a([
s() o()
], r.prototype, "components", 2); ], r.prototype, "components", 2);
r = a([ r = a([
d("create-calendar-form") h("create-calendar-form")
], r); ], r);
export { export {
r as CreateCalendarForm r as CreateCalendarForm

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

View File

@@ -543,6 +543,7 @@ const z = y.litElementPolyfillSupport;
z == null || z({ LitElement: T }); z == null || z({ LitElement: T });
(y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0"); (y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0");
export { export {
d as E,
et as f, et as f,
T as i, T as i,
j as u, j as u,

View File

@@ -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 * @license
* Copyright 2017 Google LLC * Copyright 2017 Google LLC

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

View File

@@ -160,12 +160,12 @@ table {
display: grid; display: grid;
min-height: 80px; min-height: 80px;
grid-template-areas: grid-template-areas:
". . color-chip" ". . color-chip"
"title comps color-chip" "title comps color-chip"
"description . color-chip" "description description color-chip"
"subscription-url . color-chip" "subscription-url subscription-url color-chip"
"actions . color-chip" "actions actions color-chip"
". . color-chip"; ". . color-chip";
grid-template-rows: 12px auto auto auto auto 12px; grid-template-rows: 12px auto auto auto auto 12px;
grid-template-columns: min-content auto 80px; grid-template-columns: min-content auto 80px;
color: inherit; color: inherit;
@@ -220,6 +220,8 @@ table {
.actions { .actions {
grid-area: actions; grid-area: actions;
width: fit-content; width: fit-content;
display: flex;
gap: 12px;
} }
&:hover { &:hover {

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
{% block imports %} {% block imports %}
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script> <script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script> <script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -83,9 +84,10 @@
<span class="subscription-url">{{ subscription_url }}</span> <span class="subscription-url">{{ subscription_url }}</span>
{% endif %} {% endif %}
<div class="actions"> <div class="actions">
<form action="/caldav/principal/{{ calendar.principal }}/calendar/{{ calendar.id }}" target="_blank" method="GET"> <form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
<button type="submit">Download</button> <button type="submit">Download</button>
</form> </form>
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
</div> </div>
<div class="color-chip"></div> <div class="color-chip"></div>
</a> </a>
@@ -114,6 +116,7 @@
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form"> <form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button> <button type="submit">Restore</button>
</form> </form>
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
</div> </div>
<div class="color-chip"></div> <div class="color-chip"></div>
</a> </a>
@@ -139,6 +142,7 @@
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET"> <form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET">
<button type="submit">Download</button> <button type="submit">Download</button>
</form> </form>
<delete-button trash href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
</div> </div>
</a> </a>
</li> </li>
@@ -160,6 +164,7 @@
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form"> <form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button> <button type="submit">Restore</button>
</form> </form>
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
</div> </div>
</a> </a>
</li> </li>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,8 @@ a CalDAV/CardDAV server
!!! warning !!! warning
RustiCal is **not production-ready!** RustiCal is **not production-ready!**
While I've started migrating to RustiCal and becoming more confident, please know that bugs and rough edges will still occur. I've been using it for the last few weeks and I'm slowly becoming more confident,
however you'd be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
## Features ## Features
@@ -25,3 +26,4 @@ If you still want to play around with it in its current state, absolutely feel f
- GNOME Accounts, GNOME Calendar, GNOME Contacts - GNOME Accounts, GNOME Calendar, GNOME Contacts
- Evolution - Evolution
- Apple Calendar - Apple Calendar
- Home Assistant integration

View File

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

View File

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