Compare commits

...

16 Commits

Author SHA1 Message Date
Lennart
e7138b5f8c version 0.7.0 2025-07-23 21:32:12 +02:00
Lennart
84af24a2b7 frontend: fill id with uuid for creation forms 2025-07-23 21:31:10 +02:00
Lennart
4bd6271e33 Update vtimezones-rs 2025-07-23 21:15:15 +02:00
Lennart
d817c1384c frontend: Add error handling to collection forms 2025-07-23 20:48:28 +02:00
Lennart
f8abc22e63 clippy appeasement 2025-07-23 20:41:06 +02:00
Lennart
b7b5ca4f91 Update dependencies 2025-07-23 20:31:16 +02:00
Lennart
caca2d28ed update vtimezones-rs 2025-07-23 20:23:21 +02:00
Lennart
3db2f13c1b rename vzic-rs to vtimezones-rs 2025-07-23 18:19:23 +02:00
Lennart
db01024682 add comment 2025-07-23 18:08:04 +02:00
Lennart
b2f15f2d77 fix: Add timezone-id support to mkcalendar 2025-07-23 18:04:19 +02:00
Lennart
89dd94904b frontend: Add timezone fields to calendar forms 2025-07-23 17:59:54 +02:00
Lennart
5d0263abc1 caldav: Add vtimezone repository to date timezone with timezone-id 2025-07-23 17:55:55 +02:00
Lennart
0ef3e19bd3 caldav: Fix principal collection permissions 2025-07-23 11:28:14 +02:00
Lennart
44912057fc subscription store: Correctly return whether subscription already existed 2025-07-23 11:09:48 +02:00
Lennart
c4f613a803 Add example compose.yml 2025-07-23 11:05:05 +02:00
Lennart
eb8f301e45 update dependencies 2025-07-22 17:57:24 +02:00
24 changed files with 1236 additions and 3452 deletions

454
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
members = ["crates/*"]
[workspace.package]
version = "0.6.5"
version = "0.7.0"
edition = "2024"
description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical"
@@ -96,7 +96,7 @@ strum_macros = "0.27"
serde_json = { version = "1.0", features = ["raw_value"] }
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
ical = { version = "0.11", features = ["generator", "serde"] }
toml = "0.8"
toml = "0.9"
tower = "0.5"
tower-http = { version = "0.6", features = [
"trace",
@@ -126,7 +126,7 @@ syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
heck = "0.5"
darling = "0.20"
darling = "0.21"
reqwest = { version = "0.12", features = [
"rustls-tls",
"charset",
@@ -135,6 +135,7 @@ reqwest = { version = "0.12", features = [
openidconnect = "4.0"
clap = { version = "4.5", features = ["derive", "env"] }
matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "f0591d13" }
vtimezones-rs = "0.1"
ece = { version = "2.3", default-features = false, features = [
"backend-openssl",
] }

22
compose.oidc.yml Normal file
View File

@@ -0,0 +1,22 @@
services:
rustical:
image: ghcr.io/lennart-k/rustical:latest
restart: unless-stopped
environment:
RUSTICAL_FRONTEND__ALLOW_PASSWORD_LOGIN: "false"
RUSTICAL_OIDC__NAME: "Authelia"
RUSTICAL_OIDC__ISSUER: "https://auth.example.com"
RUSTICAL_OIDC__CLIENT_ID: "{{ rustical_oidc_client_id }}"
RUSTICAL_OIDC__CLIENT_SECRET: "{{ rustical_oidc_client_secret }}"
RUSTICAL_OIDC__CLAIM_USERID: "preferred_username"
RUSTICAL_OIDC__SCOPES: '["openid", "profile", "groups"]'
RUSTICAL_OIDC__REQUIRE_GROUP: "app:rustical" # optional
RUSTICAL_OIDC__ALLOW_SIGN_UP: "true"
volumes:
- data:/var/lib/rustical
# Here you probably want to you expose instead
ports:
- 4000:4000
volumes:
data:

View File

@@ -42,3 +42,4 @@ headers.workspace = true
tower-http.workspace = true
strum.workspace = true
strum_macros.workspace = true
vtimezones-rs.workspace = true

View File

@@ -82,12 +82,25 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
request.displayname = None
}
let mut timezone = request.calendar_timezone;
if let Some(tzid) = request.calendar_timezone_id.as_ref() {
// Validate timezone id and set timezone accordingly
timezone = Some(
vtimezones_rs::VTIMEZONES
.get(tzid)
.ok_or(rustical_dav::Error::BadRequest(format!(
"Invalid timezone-id: {tzid}"
)))?
.to_string(),
);
}
let calendar = Calendar {
id: cal_id.to_owned(),
principal: principal.to_owned(),
order: request.calendar_order.unwrap_or(0),
displayname: request.displayname,
timezone: request.calendar_timezone,
timezone,
timezone_id: request.calendar_timezone_id,
color: request.calendar_color,
description: request.calendar_description,

View File

@@ -15,7 +15,6 @@ use rustical_store::Calendar;
use rustical_store::auth::Principal;
use rustical_xml::{EnumVariants, PropName};
use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::str::FromStr;
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarPropName")]
@@ -193,17 +192,22 @@ impl Resource for CalendarResource {
}
CalendarProp::CalendarTimezone(timezone) => {
// TODO: Ensure that timezone-id is also updated
// We probably want to prohibit non-IANA timezones
self.cal.timezone = timezone;
Ok(())
}
CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly),
CalendarProp::CalendarTimezoneId(timezone_id) => {
if let Some(tzid) = &timezone_id {
// Validate timezone id
chrono_tz::Tz::from_str(tzid).map_err(|_| {
rustical_dav::Error::BadRequest(format!("Invalid timezone-id: {tzid}"))
})?;
// TODO: Ensure that timezone is also updated (For now hope that clients play nice)
// Validate timezone id and set timezone accordingly
self.cal.timezone = Some(
vtimezones_rs::VTIMEZONES
.get(tzid)
.ok_or(rustical_dav::Error::BadRequest(format!(
"Invalid timezone-id: {tzid}"
)))?
.to_string(),
);
}
self.cal.timezone_id = timezone_id;
Ok(())
@@ -305,3 +309,15 @@ impl Resource for CalendarResource {
))
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_tzdb_version() {
// Ensure that both chrono_tz and vzic_rs use the same tzdb version
assert_eq!(
chrono_tz::IANA_TZDB_VERSION,
vtimezones_rs::IANA_TZDB_VERSION
);
}
}

View File

@@ -121,7 +121,7 @@ impl Resource for PrincipalResource {
}
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_read(
Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.principal.id),
))
}

View File

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

View File

@@ -1,7 +1,6 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav";
import { escapeXml } from ".";
@customElement("create-addressbook-form")
@@ -15,14 +14,12 @@ export class CreateAddressbookForm extends LitElement {
return this
}
client = createClient("/carddav")
@property()
user: string = ''
@property()
principal: string = ''
@property()
addr_id: string = ''
addr_id: string = self.crypto.randomUUID()
@property()
displayname: string = ''
@property()
@@ -49,7 +46,7 @@ export class CreateAddressbookForm extends LitElement {
<br>
<label>
id
<input type="text" name="id" @change=${e => this.addr_id = e.target.value} />
<input type="text" name="id" value=${this.addr_id} @change=${e => this.addr_id = e.target.value} />
</label>
<br>
<label>
@@ -80,8 +77,12 @@ export class CreateAddressbookForm extends LitElement {
alert("Empty displayname")
return
}
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
data: `
let response = await fetch(`/carddav/principal/${this.principal || this.user}/${this.addr_id}`, {
method: 'MKCOL',
headers: {
'Content-Type': 'application/xml'
},
body: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
@@ -91,7 +92,14 @@ export class CreateAddressbookForm extends LitElement {
</set>
</mkcol>
`
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}

View File

@@ -1,7 +1,6 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav";
import { escapeXml } from ".";
@customElement("create-calendar-form")
@@ -14,19 +13,19 @@ export class CreateCalendarForm extends LitElement {
return this
}
client = createClient("/caldav")
@property()
user: string = ''
@property()
principal: string = ''
@property()
cal_id: string = ''
cal_id: string = self.crypto.randomUUID()
@property()
displayname: string = ''
@property()
description: string = ''
@property()
timezone_id: string = ''
@property()
color: string = ''
@property()
isSubscription: boolean = false
@@ -38,7 +37,6 @@ export class CreateCalendarForm extends LitElement {
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
@@ -57,7 +55,7 @@ export class CreateCalendarForm extends LitElement {
<br>
<label>
id
<input type="text" name="id" @change=${e => this.cal_id = e.target.value} />
<input type="text" name="id" value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
</label>
<br>
<label>
@@ -65,6 +63,11 @@ export class CreateCalendarForm extends LitElement {
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${e => this.description = e.target.value} />
@@ -119,12 +122,18 @@ export class CreateCalendarForm extends LitElement {
alert("No calendar components selected")
return
}
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, {
data: `
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
method: 'MKCOL',
headers: {
'Content-Type': 'application/xml'
},
body: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ''}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
${(this.isSubscription && this.subscriptionUrl) ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ''}
@@ -136,6 +145,11 @@ export class CreateCalendarForm extends LitElement {
</mkcol>
`
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}

View File

@@ -63,7 +63,7 @@ export class EditAddressbookForm extends LitElement {
alert("Empty displayname")
return
}
await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
let response = await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
method: 'PROPPATCH',
headers: {
'Content-Type': 'application/xml'
@@ -85,6 +85,12 @@ export class EditAddressbookForm extends LitElement {
`
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}

View File

@@ -23,6 +23,8 @@ export class EditCalendarForm extends LitElement {
@property()
description: string = ''
@property()
timezone_id: string = ''
@property()
color: string = ''
@property({
converter: {
@@ -47,6 +49,11 @@ export class EditCalendarForm extends LitElement {
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
@@ -90,7 +97,7 @@ export class EditCalendarForm extends LitElement {
alert("No calendar components selected")
return
}
await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
method: 'PROPPATCH',
headers: {
'Content-Type': 'application/xml'
@@ -100,6 +107,7 @@ export class EditCalendarForm extends LitElement {
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ''}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
<CAL:supported-calendar-component-set>
@@ -116,6 +124,12 @@ export class EditCalendarForm extends LitElement {
</propertyupdate>
`
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}

View File

@@ -25,7 +25,7 @@ export default defineConfig({
format: "es",
manualChunks: {
lit: ["lit"],
webdav: ["webdav"],
// webdav: ["webdav"],
}
}
},

View File

@@ -1,7 +1,6 @@
import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { a as an } from "./webdav-D0R7xCzX.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
@@ -15,10 +14,9 @@ var __decorateClass = (decorators, target, key, kind) => {
let CreateAddressbookForm = class extends i {
constructor() {
super();
this.client = an("/carddav");
this.user = "";
this.principal = "";
this.addr_id = "";
this.addr_id = self.crypto.randomUUID();
this.displayname = "";
this.description = "";
this.dialog = e();
@@ -45,7 +43,7 @@ let CreateAddressbookForm = class extends i {
<br>
<label>
id
<input type="text" name="id" @change=${(e2) => this.addr_id = e2.target.value} />
<input type="text" name="id" value=${this.addr_id} @change=${(e2) => this.addr_id = e2.target.value} />
</label>
<br>
<label>
@@ -79,8 +77,12 @@ let CreateAddressbookForm = class extends i {
alert("Empty displayname");
return;
}
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
data: `
let response = await fetch(`/carddav/principal/${this.principal || this.user}/${this.addr_id}`, {
method: "MKCOL",
headers: {
"Content-Type": "application/xml"
},
body: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
@@ -91,6 +93,10 @@ let CreateAddressbookForm = class extends i {
</mkcol>
`
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}

View File

@@ -1,7 +1,6 @@
import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { a as an } from "./webdav-D0R7xCzX.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
@@ -15,12 +14,12 @@ var __decorateClass = (decorators, target, key, kind) => {
let CreateCalendarForm = class extends i {
constructor() {
super();
this.client = an("/caldav");
this.user = "";
this.principal = "";
this.cal_id = "";
this.cal_id = self.crypto.randomUUID();
this.displayname = "";
this.description = "";
this.timezone_id = "";
this.color = "";
this.isSubscription = false;
this.subscriptionUrl = "";
@@ -49,7 +48,7 @@ let CreateCalendarForm = class extends i {
<br>
<label>
id
<input type="text" name="id" @change=${(e2) => this.cal_id = e2.target.value} />
<input type="text" name="id" value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
</label>
<br>
<label>
@@ -57,6 +56,11 @@ let CreateCalendarForm = class extends i {
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label>
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
@@ -114,12 +118,17 @@ let CreateCalendarForm = class extends i {
alert("No calendar components selected");
return;
}
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, {
data: `
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
method: "MKCOL",
headers: {
"Content-Type": "application/xml"
},
body: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ""}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
${this.isSubscription && this.subscriptionUrl ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ""}
@@ -131,6 +140,10 @@ let CreateCalendarForm = class extends i {
</mkcol>
`
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}
@@ -150,6 +163,9 @@ __decorateClass([
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "timezone_id", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "color", 2);

View File

@@ -64,7 +64,7 @@ let EditAddressbookForm = class extends i {
alert("Empty displayname");
return;
}
await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
let response = await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
method: "PROPPATCH",
headers: {
"Content-Type": "application/xml"
@@ -85,6 +85,10 @@ let EditAddressbookForm = class extends i {
</propertyupdate>
`
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}

View File

@@ -16,6 +16,7 @@ let EditCalendarForm = class extends i {
super();
this.displayname = "";
this.description = "";
this.timezone_id = "";
this.color = "";
this.components = /* @__PURE__ */ new Set();
this.dialog = e();
@@ -35,6 +36,11 @@ let EditCalendarForm = class extends i {
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label>
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
@@ -81,7 +87,7 @@ let EditCalendarForm = class extends i {
alert("No calendar components selected");
return;
}
await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
method: "PROPPATCH",
headers: {
"Content-Type": "application/xml"
@@ -91,6 +97,7 @@ let EditCalendarForm = class extends i {
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ""}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
<CAL:supported-calendar-component-set>
@@ -107,6 +114,10 @@ let EditCalendarForm = class extends i {
</propertyupdate>
`
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}
@@ -123,6 +134,9 @@ __decorateClass([
__decorateClass([
n$1()
], EditCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "timezone_id", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "color", 2);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@
<edit-calendar-form
principal="{{ calendar.principal }}"
cal_id="{{ calendar.id }}"
timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}"
displayname="{{ calendar.displayname.as_deref().unwrap_or_default() }}"
description="{{ calendar.description.as_deref().unwrap_or_default() }}"
color="{{ calendar.color.as_deref().unwrap_or_default() }}"

View File

@@ -208,7 +208,7 @@ impl CalDateTime {
matches!(&self, Self::Date(_, _))
}
pub fn as_datetime(&self) -> Cow<DateTime<CalTimezone>> {
pub fn as_datetime(&self) -> Cow<'_, DateTime<CalTimezone>> {
match self {
Self::DateTime(datetime) => Cow::Borrowed(datetime),
Self::Date(date, tz) => Cow::Owned(

View File

@@ -22,7 +22,6 @@ chrono-tz = { workspace = true }
derive_more = { workspace = true, features = ["as_ref"] }
rustical_xml.workspace = true
tokio.workspace = true
rand.workspace = true
clap.workspace = true
rustical_dav.workspace = true
rustical_ical.workspace = true

View File

@@ -31,6 +31,11 @@ impl SubscriptionStore for SqliteStore {
}
async fn upsert_subscription(&self, sub: Subscription) -> Result<bool, Error> {
let already_exists = match self.get_subscription(&sub.id).await {
Ok(_) => true,
Err(Error::NotFound) => false,
Err(err) => return Err(err),
};
sqlx::query!(
r#"INSERT OR REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource, public_key, public_key_type, auth_secret) VALUES (?, ?, ?, ?, ?, ?, ?)"#,
sub.id,
@@ -41,8 +46,7 @@ impl SubscriptionStore for SqliteStore {
sub.public_key_type,
sub.auth_secret
).execute(&self.db).await.map_err(crate::Error::from)?;
// TODO: Correctly return whether a subscription already existed
Ok(false)
Ok(already_exists)
}
async fn delete_subscription(&self, id: &str) -> Result<(), Error> {
sqlx::query!(r#"DELETE FROM davpush_subscriptions WHERE id = ? "#, id)

View File

@@ -8,6 +8,8 @@ a CalDAV/CardDAV server
you'd still 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. :)
[Installation](installation/index.md){ .md-button }
## Features
- easy to backup, everything saved in one SQLite database