Compare commits

...

13 Commits

Author SHA1 Message Date
Lennart
283be0a26c version 0.4.5 2025-06-27 17:40:48 +02:00
Lennart
1060625b9d fix(oidc): Fix login not working for missing groups claim
see #87
2025-06-27 17:38:42 +02:00
Lennart
86ae31e94c tiny steps towards unit testing for each resource 2025-06-27 14:33:25 +02:00
Lennart
e2f5773e3c Dockerfile: Target Rust 1.88 2025-06-27 14:32:08 +02:00
Lennart
b54fbebe7c store: test preparations 2025-06-27 13:58:14 +02:00
Lennart
fe78a82806 clippy appeasement 2025-06-27 13:57:57 +02:00
Lennart
22544b8c2f Justfile: Add commands to build frontend components 2025-06-27 13:57:44 +02:00
Lennart
340b99e491 Dockerfile: Fix llvm dependency for arm64 builds 2025-06-26 22:25:04 +02:00
Lennart
787ea90376 Dockerfile, update Rust to 1.87+ 2025-06-26 22:03:16 +02:00
Lennart
973a86f21a remove some disabled and broken tests 2025-06-26 19:42:06 +02:00
Lennart
39fc2fb55d principal_type refactoring 2025-06-26 12:50:37 +02:00
Lennart
ab4d763304 tiny improvements to documentation 2025-06-26 12:39:23 +02:00
Lennart
9cf74f7198 frontend: Explicitly mark collections from other groups 2025-06-25 16:14:55 +02:00
30 changed files with 279 additions and 325 deletions

View File

@@ -2,3 +2,5 @@
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[docs/**/*.md]
indent_size = 4

23
Cargo.lock generated
View File

@@ -2754,7 +2754,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -2797,7 +2797,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2816,6 +2816,7 @@ dependencies = [
"rustical_dav_push", "rustical_dav_push",
"rustical_ical", "rustical_ical",
"rustical_store", "rustical_store",
"rustical_store_sqlite",
"rustical_xml", "rustical_xml",
"serde", "serde",
"sha2", "sha2",
@@ -2832,7 +2833,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2864,7 +2865,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2889,7 +2890,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2915,7 +2916,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -2948,7 +2949,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -2966,7 +2967,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2981,7 +2982,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3015,7 +3016,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3035,7 +3036,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.4.4" version = "0.4.5"
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.4.4" version = "0.4.5"
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"

View File

@@ -1,11 +1,11 @@
FROM --platform=$BUILDPLATFORM rust:1.86-alpine AS chef FROM --platform=$BUILDPLATFORM rust:1.88-alpine AS chef
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG BUILDPLATFORM ARG BUILDPLATFORM
# the compiler will otherwise ask for aarch64-linux-musl-gcc # the compiler will otherwise ask for aarch64-linux-musl-gcc
ENV CC_aarch64_unknown_linux_musl="clang" ENV CC_aarch64_unknown_linux_musl="clang"
ENV AR_aarch64_unknown_linux_musl="llvm-ar" ENV AR_aarch64_unknown_linux_musl="llvm20-ar"
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld" ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld"
# Stupid workaound with tempfiles since environment variables # Stupid workaound with tempfiles since environment variables
@@ -16,7 +16,7 @@ RUN case $TARGETPLATFORM in \
*) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \ *) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \
esac esac
RUN apk add --no-cache musl-dev llvm19 clang perl pkgconf make \ RUN apk add --no-cache musl-dev llvm20 clang perl pkgconf make \
&& rustup target add "$(cat /tmp/rust_target)" \ && rustup target add "$(cat /tmp/rust_target)" \
&& cargo install cargo-chef --locked \ && cargo install cargo-chef --locked \
&& rm -rf "$CARGO_HOME/registry" && rm -rf "$CARGO_HOME/registry"

View File

@@ -1,2 +1,9 @@
licenses: licenses:
cargo about generate about.hbs > crates/frontend/public/assets/licenses.html cargo about generate about.hbs > crates/frontend/public/assets/licenses.html
frontend-dev:
cd crates/frontend/js-components && deno task dev
frontend-build:
cd crates/frontend/js-components && deno task build

View File

@@ -7,6 +7,9 @@ repository.workspace = true
license.workspace = true license.workspace = true
publish = false publish = false
[dev-dependencies]
rustical_store_sqlite = { workspace = true, features = ["test"] }
[dependencies] [dependencies]
axum.workspace = true axum.workspace = true
axum-extra.workspace = true axum-extra.workspace = true

View File

@@ -11,8 +11,10 @@ mod service;
pub use service::*; pub use service::*;
mod prop; mod prop;
pub use prop::*; pub use prop::*;
#[cfg(test)]
pub mod tests;
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct PrincipalResource { pub struct PrincipalResource {
principal: Principal, principal: Principal,
members: Vec<String>, members: Vec<String>,

View File

@@ -0,0 +1,39 @@
use crate::principal::PrincipalResourceService;
use rustical_dav::resource::ResourceService;
use rustical_store::auth::{AuthenticationProvider, Principal, PrincipalType};
use rustical_store_sqlite::tests::get_test_stores;
#[tokio::test]
async fn test_principal_resource() {
let (_, cal_store, sub_store, auth_provider, _) = get_test_stores().await;
let service = PrincipalResourceService {
cal_store,
sub_store,
auth_provider: auth_provider.clone(),
};
auth_provider
.insert_principal(
Principal {
id: "user".to_owned(),
displayname: None,
memberships: vec![],
password: None,
principal_type: PrincipalType::Individual,
},
true,
)
.await
.unwrap();
assert!(matches!(
service.get_resource(&("anonymous".to_owned(),), true).await,
Err(crate::Error::NotFound)
));
let _principal_resource = service
.get_resource(&("user".to_owned(),), true)
.await
.unwrap();
}

View File

@@ -58,6 +58,7 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
Ok((result, not_found)) Ok((result, not_found))
} }
#[allow(clippy::too_many_arguments)]
pub async fn handle_addressbook_multiget<AS: AddressbookStore>( pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
addr_multiget: &AddressbookMultigetRequest, addr_multiget: &AddressbookMultigetRequest,
prop: &PropfindType<AddressObjectPropWrapperName>, prop: &PropfindType<AddressObjectPropWrapperName>,

View File

@@ -71,7 +71,10 @@
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %} {% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
<span class="title">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
</span>
<div class="comps"> <div class="comps">
{% for comp in calendar.components %} {% for comp in calendar.components %}
<span>{{ comp }}</span> <span>{{ comp }}</span>
@@ -105,7 +108,10 @@
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %} {% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
<span class="title">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
</span>
<div class="comps"> <div class="comps">
{% for comp in calendar.components %} {% for comp in calendar.components %}
<span>{{ comp }}</span> <span>{{ comp }}</span>
@@ -136,7 +142,10 @@
{% for addressbook in addressbooks %} {% for addressbook in addressbooks %}
<li class="collection-list-item"> <li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"> <a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
<span class="title">{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}</span> <span class="title">
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
</span>
<span class="description"> <span class="description">
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %} {% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
</span> </span>
@@ -158,7 +167,10 @@
{% for addressbook in deleted_addressbooks %} {% for addressbook in deleted_addressbooks %}
<li class="collection-list-item"> <li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"> <a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
<span class="title">{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}</span> <span class="title">
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
</span>
<span class="description"> <span class="description">
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %} {% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
</span> </span>

View File

@@ -7,6 +7,9 @@ pub enum OidcError {
#[error("Cannot generate redirect url, something's not configured correctly")] #[error("Cannot generate redirect url, something's not configured correctly")]
OidcParseError(#[from] ParseError), OidcParseError(#[from] ParseError),
#[error("Error fetching user info: {0}")]
UserInfo(String),
#[error(transparent)] #[error(transparent)]
OidcConfigurationError(#[from] ConfigurationError), OidcConfigurationError(#[from] ConfigurationError),

View File

@@ -41,7 +41,7 @@ struct OidcState {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
struct GroupAdditionalClaims { struct GroupAdditionalClaims {
#[serde(default)] #[serde(default)]
pub groups: Vec<String>, groups: Option<Vec<String>>,
} }
impl openidconnect::AdditionalClaims for GroupAdditionalClaims {} impl openidconnect::AdditionalClaims for GroupAdditionalClaims {}
@@ -190,12 +190,14 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
)? )?
.request_async(&http_client) .request_async(&http_client)
.await .await
.map_err(|_| OidcError::Other("Error fetching user info"))?; .map_err(|e| OidcError::UserInfo(e.to_string()))?;
if let Some(require_group) = &oidc_config.require_group { if let Some(require_group) = &oidc_config.require_group {
if !user_info_claims if !user_info_claims
.additional_claims() .additional_claims()
.groups .groups
.clone()
.unwrap_or_default()
.contains(require_group) .contains(require_group)
{ {
return Ok(( return Ok((

View File

@@ -3,7 +3,10 @@ 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}; mod principal_type;
pub use principal_type::*;
pub use principal::{AppToken, Principal};
#[async_trait] #[async_trait]
pub trait AuthenticationProvider: Send + Sync + 'static { pub trait AuthenticationProvider: Send + Sync + 'static {

View File

@@ -1,3 +1,4 @@
use crate::{Secret, auth::PrincipalType};
use axum::{ use axum::{
body::Body, body::Body,
extract::{FromRequestParts, OptionalFromRequestParts}, extract::{FromRequestParts, OptionalFromRequestParts},
@@ -6,67 +7,8 @@ use axum::{
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use derive_more::Display; use derive_more::Display;
use http::{HeaderValue, StatusCode, header}; use http::{HeaderValue, StatusCode, header};
use rustical_xml::ValueSerialize;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{convert::Infallible, fmt::Display}; use std::convert::Infallible;
use crate::Secret;
/// https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.3
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PrincipalType {
#[default]
Individual,
Group,
Resource,
Room,
Unknown,
// TODO: X-Name, IANA-token
}
impl TryFrom<&str> for PrincipalType {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"INDIVIDUAL" => Self::Individual,
"GROUP" => Self::Group,
"RESOURCE" => Self::Resource,
"ROOM" => Self::Room,
"UNKNOWN" => Self::Unknown,
_ => {
return Err(crate::Error::InvalidPrincipalType(
"Invalid principal type".to_owned(),
));
}
})
}
}
impl PrincipalType {
pub fn as_str(&self) -> &'static str {
match self {
PrincipalType::Individual => "INDIVIDUAL",
PrincipalType::Group => "GROUP",
PrincipalType::Resource => "RESOURCE",
PrincipalType::Room => "ROOM",
PrincipalType::Unknown => "UNKNOWN",
}
}
}
impl Display for PrincipalType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl ValueSerialize for PrincipalType {
fn serialize(&self) -> String {
self.to_string()
}
}
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AppToken { pub struct AppToken {

View File

@@ -0,0 +1,60 @@
use std::fmt::Display;
use rustical_xml::ValueSerialize;
use serde::{Deserialize, Serialize};
/// https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.3
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PrincipalType {
#[default]
Individual,
Group,
Resource,
Room,
Unknown,
// TODO: X-Name, IANA-token
}
impl TryFrom<&str> for PrincipalType {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"INDIVIDUAL" => Self::Individual,
"GROUP" => Self::Group,
"RESOURCE" => Self::Resource,
"ROOM" => Self::Room,
"UNKNOWN" => Self::Unknown,
_ => {
return Err(crate::Error::InvalidPrincipalType(
"Invalid principal type".to_owned(),
));
}
})
}
}
impl PrincipalType {
pub fn as_str(&self) -> &'static str {
match self {
PrincipalType::Individual => "INDIVIDUAL",
PrincipalType::Group => "GROUP",
PrincipalType::Resource => "RESOURCE",
PrincipalType::Room => "ROOM",
PrincipalType::Unknown => "UNKNOWN",
}
}
}
impl Display for PrincipalType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl ValueSerialize for PrincipalType {
fn serialize(&self) -> String {
self.to_string()
}
}

View File

@@ -11,6 +11,9 @@ mod secret;
mod subscription_store; mod subscription_store;
pub mod synctoken; pub mod synctoken;
#[cfg(test)]
pub mod tests;
pub use addressbook_store::AddressbookStore; pub use addressbook_store::AddressbookStore;
pub use calendar_store::CalendarStore; pub use calendar_store::CalendarStore;
pub use combined_calendar_store::CombinedCalendarStore; pub use combined_calendar_store::CombinedCalendarStore;

View File

View File

@@ -1,35 +0,0 @@
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;UNTIL=20370329T010000Z;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;UNTIL=20361026T010000Z;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:67d830c3e681950b6a12f7c287b316269a19fcf7
DTSTAMP:20230831T102923Z
DTSTART;TZID=Europe/Berlin:20230829T043000
DTEND;TZID=Europe/Berlin:20230829T045500
SEQUENCE:2
SUMMARY:asdjlk
TRANSP:OPAQUE
CLASS:PUBLIC
CREATED:20230831T103040Z
LAST-MODIFIED:20230831T103040Z
END:VEVENT
END:VCALENDAR

View File

@@ -1,109 +0,0 @@
BEGIN:VCALENDAR
BEGIN:VTIMEZONE
TZID:Europe/Berlin
LAST-MODIFIED:20230104T023643Z
TZURL:https://www.tzurl.org/zoneinfo/Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
X-PROLEPTIC-TZNAME:LMT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+005328
TZOFFSETTO:+0100
DTSTART:18930401T000632
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19160430T230000
RDATE:19400401T020000
RDATE:19430329T020000
RDATE:19460414T020000
RDATE:19470406T030000
RDATE:19480418T020000
RDATE:19490410T020000
RDATE:19800406T020000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19161001T010000
RDATE:19421102T030000
RDATE:19431004T030000
RDATE:19441002T030000
RDATE:19451118T030000
RDATE:19461007T030000
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19170416T020000
RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19170917T030000
RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19440403T020000
RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEMT
TZOFFSETFROM:+0200
TZOFFSETTO:+0300
DTSTART:19450524T010000
RDATE:19470511T020000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0300
TZOFFSETTO:+0200
DTSTART:19450924T030000
RDATE:19470629T030000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0100
TZOFFSETTO:+0100
DTSTART:19460101T000000
RDATE:19800101T000000
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19471005T030000
RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19800928T030000
RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
END:VCALENDAR

View File

@@ -1,46 +0,0 @@
// use rstest::rstest;
// use rstest_reuse::{self, apply, template};
// use rustical_store::{CalendarObject, CalendarStore};
// use rustical_store_sqlite::{calendar_store::SqliteCalendarStore, create_test_db};
//
// const TIMEZONE: &str = include_str!("examples/timezone.ics");
// const EVENT: &str = include_str!("examples/event.ics");
//
// #[template]
// #[rstest]
// #[case::sqlite(async {
// let (send, _recv) = tokio::sync::mpsc::channel(100);
// SqliteCalendarStore::new(create_test_db().await.unwrap(), send)
// })]
// async fn cal_store<CS: CalendarStore>(
// #[future(awt)]
// #[case]
// mut store: CS,
// ) {
// }
// TODO: Reimplement, add test principal
// #[apply(cal_store)]
// #[tokio::test]
// async fn test_create_event<CS: CalendarStore>(store: CS) {
// store
// .insert_calendar(rustical_store::Calendar {
// id: "test".to_owned(),
// displayname: Some("Test Calendar".to_owned()),
// principal: "testuser".to_owned(),
// timezone: Some(TIMEZONE.to_owned()),
// ..Default::default() // timezone: TIMEZONE.to_owned(),
// })
// .await
// .unwrap();
//
// let object = CalendarObject::from_ics("asd".to_owned(), EVENT.to_owned()).unwrap();
// store
// .put_object("testuser".to_owned(), "test".to_owned(), object, true)
// .await
// .unwrap();
//
// let event = store.get_object("testuser", "test", "asd").await.unwrap();
// assert_eq!(event.get_ics(), EVENT);
// assert_eq!(event.get_id(), "asd");
// }

View File

@@ -7,6 +7,9 @@ repository.workspace = true
license.workspace = true license.workspace = true
publish = false publish = false
[features]
test = []
[dependencies] [dependencies]
tokio.workspace = true tokio.workspace = true
rustical_store = { workspace = true } rustical_store = { workspace = true }

View File

@@ -8,6 +8,9 @@ pub mod error;
pub mod principal_store; pub mod principal_store;
pub mod subscription_store; pub mod subscription_store;
#[cfg(any(test, feature = "test"))]
pub mod tests;
#[derive(Debug, Clone, Serialize, sqlx::Type)] #[derive(Debug, Clone, Serialize, sqlx::Type)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub(crate) enum ChangeOperation { pub(crate) enum ChangeOperation {
@@ -42,13 +45,3 @@ pub async fn create_db_pool(db_url: &str, migrate: bool) -> Result<Pool<Sqlite>,
} }
Ok(db) Ok(db)
} }
pub async fn create_test_db() -> Result<SqlitePool, sqlx::Error> {
let db = SqlitePool::connect("sqlite::memory:").await?;
sqlx::migrate!("./migrations").run(&db).await?;
Ok(db)
}
pub async fn create_test_store() -> Result<SqliteStore, sqlx::Error> {
Ok(SqliteStore::new(create_test_db().await?))
}

View File

@@ -0,0 +1,42 @@
use crate::{
SqliteStore, addressbook_store::SqliteAddressbookStore, calendar_store::SqliteCalendarStore,
principal_store::SqlitePrincipalStore,
};
use rustical_store::{
AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore,
auth::AuthenticationProvider,
};
use sqlx::SqlitePool;
use std::sync::Arc;
use tokio::sync::mpsc::Receiver;
pub async fn get_test_stores() -> (
Arc<impl AddressbookStore>,
Arc<impl CalendarStore>,
Arc<impl SubscriptionStore>,
Arc<impl AuthenticationProvider>,
Receiver<CollectionOperation>,
) {
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::migrate!("./migrations").run(&db).await.unwrap();
// let db = create_db_pool("sqlite::memory:", true).await.unwrap();
// Channel to watch for changes (for DAV Push)
let (send, recv) = tokio::sync::mpsc::channel(1000);
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
let principal_store = Arc::new(SqlitePrincipalStore::new(db.clone()));
(
addressbook_store,
cal_store,
subscription_store,
principal_store,
recv,
)
}
#[tokio::test]
async fn test_create_store() {
get_test_stores().await;
}

View File

@@ -0,0 +1,7 @@
# Frontend
The frontend is currently generated through [askama templates](https://askama.readthedocs.io/en/stable/) for server-side rendered pages
and uses Web Components for interactive elements.
Normally, content that will be statically served by the frontend module (i.e. stylesheet and web components) is embedded into the binary.
Using the `frontend-dev` feature you can serve it from source to see changes without recompiling RustiCal.

View File

@@ -3,11 +3,11 @@
Collection of RFCs relevant to this project Collection of RFCs relevant to this project
- Versioning Extensions to WebDAV: [RFC 3253](https://datatracker.ietf.org/doc/html/rfc3253) - Versioning Extensions to WebDAV: [RFC 3253](https://datatracker.ietf.org/doc/html/rfc3253)
- provides the REPORT method - provides the REPORT method
- Calendaring Extensions to WebDAV (CalDAV): [RFC 4791](https://datatracker.ietf.org/doc/html/rfc4791) - Calendaring Extensions to WebDAV (CalDAV): [RFC 4791](https://datatracker.ietf.org/doc/html/rfc4791)
- Scheduling Extensions to CalDAV: [RFC 6638](https://datatracker.ietf.org/doc/html/rfc6638) - Scheduling Extensions to CalDAV: [RFC 6638](https://datatracker.ietf.org/doc/html/rfc6638)
- not sur`e yet whether to implement this - not sure yet whether to implement this
- Collection Synchronization WebDAV [RFC 6578](https://datatracker.ietf.org/doc/html/rfc6578) - Collection Synchronization WebDAV [RFC 6578](https://datatracker.ietf.org/doc/html/rfc6578)
- We need to implement sync-token, etc. - We need to implement sync-token, etc.
- This is important for more efficient synchronisation - This is important for more efficient synchronisation
- iCalendar [RFC 2445](https://datatracker.ietf.org/doc/html/rfc2445#section-3.10) - iCalendar [RFC 2445](https://datatracker.ietf.org/doc/html/rfc2445#section-3.10)

View File

@@ -3,10 +3,10 @@
a CalDAV/CardDAV server a CalDAV/CardDAV server
!!! warning !!! warning
RustiCal is **not production-ready!** RustiCal is **not production-ready!**
I've been using it for the last few weeks and I'm slowly becoming more confident, 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. 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

View File

@@ -0,0 +1,31 @@
# Configuration
While RustiCal (apart from user management) will work without any configuration you should still know how to configure it. :)
You can either mount a `config.toml` file or use environment variables (recommended).
To see the options you can generate a default configuration using
```sh title="Generate default config.toml"
rustical gen-config
```
To see all configuration options available you can browse the [Cargo docs](/rustical/_crate/rustical/config/struct.Config.html).
## Environment variables
The options in `config.toml` can also be configured using environment variables.
Names translate the following:
```toml title="Example config.toml"
[data_store.toml]
path = "asd"
```
becomes `RUSTICAL_DATA_STORE__TOML__PATH`.
Every variable is
- uppercase
- prefixed by `RUSTICAL_`
- Dots become `__`
- Arrays are JSON-encoded

View File

@@ -40,38 +40,6 @@ App tokens are used by your CalDAV/CardDAV client (which can be managed through
I recommend to generate random app tokens for each CalDAV/CardDAV client. I recommend to generate random app tokens for each CalDAV/CardDAV client.
Since the app tokens are random they use the faster `pbkdf2` algorithm. Since the app tokens are random they use the faster `pbkdf2` algorithm.
## Configuration
While RustiCal (apart from user management) will work without any configuration you should still know how to configure it. :)
You can either mount a `config.toml` file or use environment variables.
To see the options you can generate a default configuration using
```sh title="Generate default config.toml"
rustical gen-config
```
To see all configuration options available you can browse the [Cargo docs](/rustical/_crate/rustical/config/struct.Config.html).
### Environment variables
The options in `config.toml` can also be configured using environment variables.
Names translate the following:
```toml title="Example config.toml"
[data_store.toml]
path = "asd"
```
becomes `RUSTICAL_DATA_STORE__TOML__PATH`.
Every variable is
- uppercase
- prefixed by `RUSTICAL_`
- Dots become `__`
- Arrays are JSON-encoded
## Manual ## Manual
```sh ```sh

10
docs/style.css Normal file
View File

@@ -0,0 +1,10 @@
body .md-main {
h1 {
font-weight: bold;
color: var(--md-typeset-color);
}
}
.md-tabs {
box-shadow: 0px 0 20px -10px black;
}

View File

@@ -14,7 +14,7 @@ theme:
name: Switch to light mode name: Switch to light mode
- media: "(prefers-color-scheme: light)" - media: "(prefers-color-scheme: light)"
scheme: default scheme: default
primary: indigo primary: white
accent: indigo accent: indigo
toggle: toggle:
icon: material/toggle-switch icon: material/toggle-switch
@@ -38,6 +38,13 @@ theme:
- content.tooltips - content.tooltips
- navigation.indices - navigation.indices
- navigation.tabs - navigation.tabs
- navigation.indexes
- navigation.indexes
- navigation.instant
- navigation.footer
extra_css:
- style.css
markdown_extensions: markdown_extensions:
- fenced_code - fenced_code
@@ -58,10 +65,13 @@ markdown_extensions:
nav: nav:
- Home: index.md - Home: index.md
- Installation: installation.md - Installation:
- installation/index.md
- Configuration: installation/configuration.md
- OpenID Connect: setup/oidc.md - OpenID Connect: setup/oidc.md
- Developers: - Developers:
- developers/index.md - developers/index.md
- Relevant RFCs: developers/rfcs.md - Relevant RFCs: developers/rfcs.md
- Frontend: developers/frontend.md
- Debugging: developers/debugging.md - Debugging: developers/debugging.md
- Cargo docs: /rustical/_crate/rustical/ - Cargo docs: /rustical/_crate/rustical/