Compare commits

..

6 Commits

14 changed files with 153 additions and 27 deletions

22
Cargo.lock generated
View File

@@ -2999,7 +2999,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -3042,7 +3042,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3080,7 +3080,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3112,7 +3112,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3137,7 +3137,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3163,7 +3163,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -3196,7 +3196,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -3214,7 +3214,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3229,7 +3229,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3263,7 +3263,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.4.10" version = "0.4.11"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3284,7 +3284,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.4.10" version = "0.4.11"
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.10" version = "0.4.11"
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

@@ -38,7 +38,12 @@ export class CreateAddressbookForm extends LitElement {
<form @submit=${this.submit} ${ref(this.form)}> <form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
principal (for group addressbooks) principal (for group addressbooks)
<input type="text" name="principal" value=${this.user} @change=${e => this.principal = e.target.value} /> <select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map(membership => html`
<option value=${membership}>${membership}</option>
`)}
</select>
</label> </label>
<br> <br>
<label> <label>

View File

@@ -28,6 +28,8 @@ export class CreateCalendarForm extends LitElement {
@property() @property()
color: String = '' color: String = ''
@property() @property()
isSubscription: boolean = false
@property()
subscriptionUrl: String = '' subscriptionUrl: String = ''
@property() @property()
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set() components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
@@ -43,8 +45,13 @@ export class CreateCalendarForm extends LitElement {
<h3>Create calendar</h3> <h3>Create calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}> <form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
principal (for group calendar) principal (for group calendars)
<input type="text" name="principal" value=${this.user} @change=${e => this.principal = e.target.value} /> <select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map(membership => html`
<option value=${membership}>${membership}</option>
`)}
</select>
</label> </label>
<br> <br>
<label> <label>
@@ -67,11 +74,20 @@ export class CreateCalendarForm extends LitElement {
<input type="color" name="color" @change=${e => this.color = e.target.value} /> <input type="color" name="color" @change=${e => this.color = e.target.value} />
</label> </label>
<br> <br>
<br>
<label> <label>
Subscription URL Calendar is subscription to external calendar
<input type="text" name="subscription_url" @change=${e => this.subscriptionUrl = e.target.value} /> <input type="checkbox" name="is_subscription" @change=${e => this.isSubscription = e.target.checked} />
</label> </label>
<br> <br>
${this.isSubscription ? html`
<label>
Subscription URL
<input type="text" name="subscription_url" @change=${e => this.subscriptionUrl = e.target.value} />
</label>
<br>
`: html``}
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html` ${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
<label> <label>
Support ${comp} Support ${comp}
@@ -110,7 +126,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><href>${this.subscriptionUrl}</href></CS:source>` : ''} ${(this.isSubscription && 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

@@ -1,6 +1,5 @@
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 { createClient } from "webdav";
@customElement("delete-button") @customElement("delete-button")
export class DeleteButton extends LitElement { export class DeleteButton extends LitElement {

View File

@@ -0,0 +1,9 @@
interface Window {
rusticalUser: {
id: String,
displayname: String | null,
memberships: Array<String>,
principal_type: "individual" | "group" | "room" | String
}
}

View File

@@ -35,7 +35,12 @@ let CreateAddressbookForm = class extends i {
<form @submit=${this.submit} ${n(this.form)}> <form @submit=${this.submit} ${n(this.form)}>
<label> <label>
principal (for group addressbooks) principal (for group addressbooks)
<input type="text" name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value} /> <select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map((membership) => x`
<option value=${membership}>${membership}</option>
`)}
</select>
</label> </label>
<br> <br>
<label> <label>

View File

@@ -22,6 +22,7 @@ let CreateCalendarForm = class extends i {
this.displayname = ""; this.displayname = "";
this.description = ""; this.description = "";
this.color = ""; this.color = "";
this.isSubscription = false;
this.subscriptionUrl = ""; this.subscriptionUrl = "";
this.components = /* @__PURE__ */ new Set(); this.components = /* @__PURE__ */ new Set();
this.dialog = e(); this.dialog = e();
@@ -37,8 +38,13 @@ let CreateCalendarForm = class extends i {
<h3>Create calendar</h3> <h3>Create calendar</h3>
<form @submit=${this.submit} ${n(this.form)}> <form @submit=${this.submit} ${n(this.form)}>
<label> <label>
principal (for group calendar) principal (for group calendars)
<input type="text" name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value} /> <select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map((membership) => x`
<option value=${membership}>${membership}</option>
`)}
</select>
</label> </label>
<br> <br>
<label> <label>
@@ -61,11 +67,20 @@ let CreateCalendarForm = class extends i {
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} /> <input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
</label> </label>
<br> <br>
<br>
<label> <label>
Subscription URL Calendar is subscription to external calendar
<input type="text" name="subscription_url" @change=${(e2) => this.subscriptionUrl = e2.target.value} /> <input type="checkbox" name="is_subscription" @change=${(e2) => this.isSubscription = e2.target.checked} />
</label> </label>
<br> <br>
${this.isSubscription ? x`
<label>
Subscription URL
<input type="text" name="subscription_url" @change=${(e2) => this.subscriptionUrl = e2.target.value} />
</label>
<br>
` : x``}
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x` ${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
<label> <label>
Support ${comp} Support ${comp}
@@ -107,7 +122,7 @@ let CreateCalendarForm = class extends i {
<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><href>${this.subscriptionUrl}</href></CS:source>` : ""} ${this.isSubscription && 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>
@@ -138,6 +153,9 @@ __decorateClass([
__decorateClass([ __decorateClass([
n$1() n$1()
], CreateCalendarForm.prototype, "color", 2); ], CreateCalendarForm.prototype, "color", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "isSubscription", 2);
__decorateClass([ __decorateClass([
n$1() n$1()
], CreateCalendarForm.prototype, "subscriptionUrl", 2); ], CreateCalendarForm.prototype, "subscriptionUrl", 2);

View File

@@ -310,12 +310,20 @@ footer {
} }
input[type="text"], input[type="text"],
input[type="password"] { input[type="password"],
input[type="color"],
select {
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%); background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
border: 2px solid var(--border-color); border: 2px solid var(--border-color);
padding: 6px 6px; padding: 6px 6px;
color: var(--text-on-background-color); color: var(--text-on-background-color);
margin: 2px; margin: 2px;
border-radius: 8px;
&:hover,
&:focus {
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 20%);
}
} }
svg.icon { svg.icon {

View File

@@ -1,6 +1,10 @@
{% extends "layouts/default.html" %} {% extends "layouts/default.html" %}
{% block imports %} {% block imports %}
<template id="data-rustical-user">{{ user|json }}</template>
<script>
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
</script>
<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> <script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>

View File

@@ -25,6 +25,7 @@ pub struct Principal {
pub displayname: Option<String>, pub displayname: Option<String>,
#[serde(default)] #[serde(default)]
pub principal_type: PrincipalType, pub principal_type: PrincipalType,
#[serde(skip_serializing)]
pub password: Option<Secret<String>>, pub password: Option<Secret<String>>,
#[serde(default)] #[serde(default)]
pub memberships: Vec<String>, pub memberships: Vec<String>,

View File

@@ -206,7 +206,7 @@ impl AuthenticationProvider for SqlitePrincipalStore {
None, None,
None, None,
Params { Params {
rounds: 100, rounds: 10,
..Default::default() ..Default::default()
}, },
&salt, &salt,

60
docs/setup/client.md Normal file
View File

@@ -0,0 +1,60 @@
# Client Setup
## Common
Following resources are available.
```
/.well-known/caldav
# CalDAV root
/caldav
# Principal home
/caldav/principal/<user_id>
# Calendar home
/caldav/principal/<user_id>/<calendar_id>
/caldav/principal/<user_id>/_birthdays_<addressbook_id>
```
```
/.well-known/carddav
# CardDAV root
/carddav
# Principal home
/carddav/principal/<user_id>
# Addressbook home
/carddav/principal/<user_id>/<addressbook_id>
```
### Authentication
Authenticate with HTTP Basic authentication using your user id and a generated app token.
## DAVx5
You can set up DAVx5 through the Nextcloud login flow. Collections including group collections will automatically be discovered.
## Apple Calendar
You can download a configuration profile from the frontend in the app token section.
**Limitation**: Group collections are not automatically discovered, for these you need to set up separate CalDAV configurations using the corresponding principal homes (but your own user id).
## Evolution
Set up a collection account in the account settings.
**Limitation**: Group collections are not discovered. It seems as if currently you have to add each group collection manually.
## Home Assistant CalDAV integration
As URL specify
```
https://<your-host>/.well-known/caldav
```
For goup collections explicitly specify
```
https://<your-host>/caldav/principal/<principal>
```

View File

@@ -68,6 +68,7 @@ nav:
- Installation: - Installation:
- installation/index.md - installation/index.md
- Configuration: installation/configuration.md - Configuration: installation/configuration.md
- Client Setup: setup/client.md
- OpenID Connect: setup/oidc.md - OpenID Connect: setup/oidc.md
- Developers: - Developers:
- developers/index.md - developers/index.md