Compare commits

...

25 Commits

Author SHA1 Message Date
Lennart
d2f5f7c89b version 0.11.0 2025-12-05 15:06:01 +01:00
Lennart Kämmle
15e431ce12 Merge pull request #138 from lennart-k/feature/birthday-calendar
Feature/birthday calendar
2025-12-05 15:03:56 +01:00
Lennart
96a16951f4 sqlx prepare 2025-12-05 14:55:30 +01:00
Lennart
a32b766c0c Merge branch 'main' into feature/birthday-calendar 2025-12-05 14:51:51 +01:00
Lennart
7a101b7364 frontend: Fix cursor for anchors 2025-12-05 14:51:34 +01:00
Lennart
57275a10b4 Add birthday calendar creation to frontend 2025-12-05 14:50:02 +01:00
Lennart
af239e34bf birthday calendar store: Support manual birthday calendar creation 2025-12-05 14:49:09 +01:00
Lennart
e99b1d9123 calendar resource: Remove prop write guards 2025-12-05 14:48:35 +01:00
Lennart
e39657eb29 PROPPATCH: Fix privileges 2025-12-05 14:48:11 +01:00
Lennart
607db62859 Merge branch 'main' into feature/birthday-calendar 2025-12-05 11:47:42 +01:00
Lennart
d5c1ddc590 caldav: Update test_propfind regression test 2025-11-22 18:49:32 +01:00
Lennart
a79e1901b8 test_propfind: Revert assert_eq order 2025-11-22 18:48:36 +01:00
Lennart
f29c8fa925 Merge branch 'main' into feature/birthday-calendar 2025-11-22 18:46:59 +01:00
Lennart
96f221f721 birthday_calendar: Refactor insert_birthday_calendar 2025-11-22 18:35:26 +01:00
Lennart
ba3b64a9c4 Merge branch 'main' into feature/birthday-calendar 2025-11-22 18:30:44 +01:00
Lennart
873b40ad10 stylesheet: Add flex-wrap to actions 2025-11-05 16:05:55 +01:00
Lennart
5588137f73 sqlx prepare 2025-11-04 17:01:54 +01:00
Lennart
7bf00da0e5 implement deleting and restoring birthday calendars 2025-11-04 16:56:17 +01:00
Lennart
be08275cd3 Merge branch 'main' into feature/birthday-calendar 2025-11-04 16:28:08 +01:00
Lennart
381af1b877 run .sqlx prepare 2025-11-03 15:37:40 +01:00
Lennart
425d10cb99 CalendarStore::is_read_only now refers to its content only and not its metadata 2025-11-02 21:07:06 +01:00
Lennart
5cdbb3b9d3 migrate birthday store to sqlite 2025-11-02 21:06:43 +01:00
Lennart
547e477eca make sure a birthday calendar will be created for each addressbook 2025-11-02 21:05:31 +01:00
Lennart
c19c3492c3 frontend: Remove birthday calendar guard 2025-11-02 20:45:58 +01:00
Lennart
5878b93d62 add birthday_calendar table migrations 2025-11-02 20:45:31 +01:00
32 changed files with 989 additions and 259 deletions

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone_id = ?, push_topic = ?\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e"
}

View File

@@ -0,0 +1,74 @@
{
"db_name": "SQLite",
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?)\n ",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "displayname",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "order",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "color",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "timezone_id",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 7,
"type_info": "Datetime"
},
{
"name": "addr_synctoken",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "push_topic",
"ordinal": 9,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
true,
true,
false,
true,
true,
true,
false,
false
]
},
"hash": "525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc"
}

View File

@@ -0,0 +1,74 @@
{
"db_name": "SQLite",
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE principal = ?\n AND (\n (deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted\n OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted\n )\n ",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "displayname",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "order",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "color",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "timezone_id",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 7,
"type_info": "Datetime"
},
{
"name": "addr_synctoken",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "push_topic",
"ordinal": 9,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
true,
true,
false,
true,
true,
true,
false,
false
]
},
"hash": "66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO birthday_calendars (principal, id, displayname, description, \"order\", color, push_topic)\n VALUES (?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a"
}

25
Cargo.lock generated
View File

@@ -3047,7 +3047,7 @@ dependencies = [
[[package]]
name = "rustical"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"anyhow",
"argon2",
@@ -3090,7 +3090,7 @@ dependencies = [
[[package]]
name = "rustical_caldav"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"async-std",
"async-trait",
@@ -3131,7 +3131,7 @@ dependencies = [
[[package]]
name = "rustical_carddav"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"async-trait",
"axum",
@@ -3163,7 +3163,7 @@ dependencies = [
[[package]]
name = "rustical_dav"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"async-trait",
"axum",
@@ -3188,7 +3188,7 @@ dependencies = [
[[package]]
name = "rustical_dav_push"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"async-trait",
"axum",
@@ -3213,7 +3213,7 @@ dependencies = [
[[package]]
name = "rustical_frontend"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"askama",
"askama_web",
@@ -3249,7 +3249,7 @@ dependencies = [
[[package]]
name = "rustical_ical"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"axum",
"chrono",
@@ -3266,7 +3266,7 @@ dependencies = [
[[package]]
name = "rustical_oidc"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"async-trait",
"axum",
@@ -3282,7 +3282,7 @@ dependencies = [
[[package]]
name = "rustical_store"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"anyhow",
"async-trait",
@@ -3315,7 +3315,7 @@ dependencies = [
[[package]]
name = "rustical_store_sqlite"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"async-trait",
"chrono",
@@ -3327,6 +3327,7 @@ dependencies = [
"rustical_ical",
"rustical_store",
"serde",
"sha2",
"sqlx",
"thiserror 2.0.17",
"tokio",
@@ -3336,7 +3337,7 @@ dependencies = [
[[package]]
name = "rustical_xml"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"quick-xml",
"thiserror 2.0.17",
@@ -5113,7 +5114,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "xml_derive"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"darling 0.23.0",
"heck",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"]
[workspace.package]
version = "0.10.5"
version = "0.11.0"
rust-version = "1.91"
edition = "2024"
description = "A CalDAV server"

View File

@@ -188,9 +188,6 @@ impl Resource for CalendarResource {
}
fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
if self.read_only {
return Err(rustical_dav::Error::PropReadOnly);
}
match prop {
CalendarPropWrapper::Calendar(prop) => match prop {
CalendarProp::CalendarColor(color) => {
@@ -263,9 +260,6 @@ impl Resource for CalendarResource {
}
fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
if self.read_only {
return Err(rustical_dav::Error::PropReadOnly);
}
match prop {
CalendarPropWrapperName::Calendar(prop) => match prop {
CalendarPropName::CalendarColor => {
@@ -317,16 +311,11 @@ impl Resource for CalendarResource {
}
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
if self.cal.subscription_url.is_some() {
if self.cal.subscription_url.is_some() || self.read_only {
return Ok(UserPrivilegeSet::owner_write_properties(
user.is_principal(&self.cal.principal),
));
}
if self.read_only {
return Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.cal.principal),
));
}
Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.cal.principal),

View File

@@ -211,6 +211,9 @@ END:VCALENDAR
<privilege>
<read/>
</privilege>
<privilege>
<write-properties/>
</privilege>
<privilege>
<read-acl/>
</privilege>

View File

@@ -39,7 +39,7 @@ async fn test_propfind() {
.unwrap()
.trim()
.replace("\r\n", "\n");
similar_asserts::assert_eq!(output, expected_output);
similar_asserts::assert_eq!(expected_output, output);
}
}
}

View File

@@ -88,7 +88,7 @@ pub async fn route_proppatch<R: ResourceService>(
.get_resource(path_components, false)
.await?;
let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Write) {
if !privileges.has(&UserPrivilege::WriteProperties) {
return Err(Error::Unauthorized.into());
}

View File

@@ -0,0 +1,102 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { escapeXml } from ".";
import { getTimezones } from "./timezones.ts";
@customElement("create-birthday-calendar-form")
export class CreateCalendarForm extends LitElement {
protected override createRenderRoot() {
return this
}
@property()
principal: string = ''
@property()
addr_id: string = ''
@property()
displayname: string = ''
@property()
description: string = ''
@property()
color: string = ''
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
@property()
timezones: Array<String> = []
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
<dialog ${ref(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${e => this.description = e.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${e => this.color = e.target.value} />
</label>
<br>
<button type="submit">Create</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
</form>
</dialog>
`
}
async submit(e: SubmitEvent) {
e.preventDefault()
if (!this.addr_id) {
alert("Empty id")
return
}
if (!this.displayname) {
alert("Empty displayname")
return
}
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_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.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>
<CAL:comp name="VEVENT" />
</CAL:supported-calendar-component-set>
</prop>
</set>
</mkcol>
`
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}
}
declare global {
interface HTMLElementTagNameMap {
'create-calendar-form': CreateCalendarForm
}
}

View File

@@ -14,6 +14,7 @@ export default defineConfig({
rollupOptions: {
input: [
"lib/create-birthday-calendar-form.ts",
"lib/create-calendar-form.ts",
"lib/edit-calendar-form.ts",
"lib/import-calendar-form.ts",

View File

@@ -0,0 +1,122 @@
import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let CreateCalendarForm = class extends i {
constructor() {
super(...arguments);
this.principal = "";
this.addr_id = "";
this.displayname = "";
this.description = "";
this.color = "";
this.dialog = e();
this.form = e();
this.timezones = [];
}
createRenderRoot() {
return this;
}
render() {
return x`
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
<dialog ${n(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${n(this.form)}>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
</label>
<br>
<button type="submit">Create</button>
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}} class="cancel">Cancel</button>
</form>
</dialog>
`;
}
async submit(e2) {
e2.preventDefault();
if (!this.addr_id) {
alert("Empty id");
return;
}
if (!this.displayname) {
alert("Empty displayname");
return;
}
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_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.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>
<CAL:comp name="VEVENT" />
</CAL:supported-calendar-component-set>
</prop>
</set>
</mkcol>
`
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}
};
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "principal", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "addr_id", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "displayname", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "color", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "timezones", 2);
CreateCalendarForm = __decorateClass([
t("create-birthday-calendar-form")
], CreateCalendarForm);
export {
CreateCalendarForm
};

View File

@@ -53,6 +53,11 @@ a {
color: var(--text-on-background-color);
}
a,
button {
cursor: pointer;
}
header {
background: var(--background-darker);
min-height: 60px;
@@ -239,10 +244,8 @@ ul.collection-list {
z-index: 1;
pointer-events: none;
a,
button {
pointer-events: all;
cursor: pointer;
}
.title {
@@ -282,6 +285,7 @@ ul.collection-list {
grid-area: actions;
width: fit-content;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
}

View File

@@ -1,6 +1,6 @@
<h2>{{user.id }}'s Addressbooks</h2>
<ul class="collection-list">
{% for (meta, addressbook) in addressbooks %}
{% for (meta, birthday_cal, addressbook) in addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner">
@@ -24,9 +24,17 @@
></edit-addressbook-form>
<delete-button trash
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
{% if !birthday_cal.is_some() %}
<create-birthday-calendar-form
principal="{{ addressbook.principal }}"
addr_id="{{ addressbook.id }}"
displayname="{{ addressbook.displayname.as_deref().unwrap_or_default() }} birthdays"
></create-birthday-calendar-form>
{% endif %}
</div>
<div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
{{ birthday_cal.is_some() }}
</div>
</div>
</li>
@@ -37,7 +45,7 @@
{%if !deleted_addressbooks.is_empty() %}
<h3>Deleted Addressbooks</h3>
<ul class="collection-list">
{% for (meta, addressbook) in deleted_addressbooks %}
{% for (meta, birthday_cal, addressbook) in deleted_addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner">

View File

@@ -24,7 +24,6 @@
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
<button type="submit">Download</button>
</form>
{% if !calendar.id.starts_with("_birthdays_") %}
<edit-calendar-form
principal="{{ calendar.principal }}"
cal_id="{{ calendar.id }}"
@@ -35,7 +34,6 @@
components="{{ calendar.components | json }}"
></edit-calendar-form>
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
{% endif %}
</div>
<div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects

View File

@@ -5,6 +5,7 @@
<script>
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
</script>
<script type="module" src="/frontend/assets/js/create-birthday-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/edit-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script>

View File

@@ -12,7 +12,7 @@ use http::{Method, StatusCode};
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
use rustical_store::{
AddressbookStore, CalendarStore,
AddressbookStore, CalendarStore, PrefixedCalendarStore,
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
};
use std::sync::Arc;
@@ -39,7 +39,11 @@ use crate::routes::{
#[cfg(not(feature = "dev"))]
use assets::{Assets, EmbedService};
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
pub fn frontend_router<
AP: AuthenticationProvider,
CS: CalendarStore,
AS: AddressbookStore + PrefixedCalendarStore,
>(
prefix: &'static str,
auth_provider: Arc<AP>,
cal_store: Arc<CS>,

View File

@@ -1,10 +1,12 @@
use std::sync::Arc;
use askama::Template;
use askama_web::WebTemplate;
use axum::{Extension, extract::Path, response::IntoResponse};
use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal};
use rustical_store::{
Addressbook, AddressbookStore, Calendar, CollectionMetadata, PrefixedCalendarStore,
auth::Principal,
};
use std::sync::Arc;
use crate::pages::user::{Section, UserPage};
@@ -18,11 +20,11 @@ impl Section for AddressbooksSection {
#[template(path = "components/sections/addressbooks_section.html")]
pub struct AddressbooksSection {
pub user: Principal,
pub addressbooks: Vec<(CollectionMetadata, Addressbook)>,
pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>,
pub addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
pub deleted_addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
}
pub async fn route_addressbooks<AS: AddressbookStore>(
pub async fn route_addressbooks<AS: AddressbookStore + PrefixedCalendarStore>(
Path(user_id): Path<String>,
Extension(addr_store): Extension<Arc<AS>>,
user: Principal,
@@ -43,22 +45,42 @@ pub async fn route_addressbooks<AS: AddressbookStore>(
let mut addressbook_infos = vec![];
for addressbook in addressbooks {
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
let birthday_cal = match addr_store
.get_calendar(&addressbook.principal, &birthday_id, true)
.await
{
Ok(cal) => Some(cal),
Err(rustical_store::Error::NotFound) => None,
err => Some(err.unwrap()),
};
addressbook_infos.push((
addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id)
.await
.unwrap(),
birthday_cal,
addressbook,
));
}
let mut deleted_addressbook_infos = vec![];
for addressbook in deleted_addressbooks {
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
let birthday_cal = match addr_store
.get_calendar(&addressbook.principal, &birthday_id, true)
.await
{
Ok(cal) => Some(cal),
Err(rustical_store::Error::NotFound) => None,
err => Some(err.unwrap()),
};
deleted_addressbook_infos.push((
addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id)
.await
.unwrap(),
birthday_cal,
addressbook,
));
}

View File

@@ -98,5 +98,6 @@ pub trait CalendarStore: Send + Sync + 'static {
object_id: &str,
) -> Result<(), Error>;
// read_only refers to objects, metadata may still be updated
fn is_read_only(&self, cal_id: &str) -> bool;
}

View File

@@ -1,209 +0,0 @@
use crate::{
Addressbook, AddressbookStore, Calendar, CalendarStore, Error, calendar::CalendarMetadata,
combined_calendar_store::PrefixedCalendarStore,
};
use async_trait::async_trait;
use derive_more::derive::Constructor;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
use sha2::{Digest, Sha256};
use std::{collections::HashMap, sync::Arc};
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
#[derive(Constructor, Clone)]
pub struct ContactBirthdayStore<AS: AddressbookStore>(Arc<AS>);
impl<AS: AddressbookStore> PrefixedCalendarStore for ContactBirthdayStore<AS> {
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
}
fn birthday_calendar(addressbook: Addressbook) -> Calendar {
Calendar {
principal: addressbook.principal,
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
meta: CalendarMetadata {
displayname: addressbook
.displayname
.map(|name| format!("{name} birthdays")),
order: 0,
description: None,
color: None,
},
timezone_id: None,
deleted_at: addressbook.deleted_at,
synctoken: addressbook.synctoken,
subscription_url: None,
push_topic: {
let mut hasher = Sha256::new();
hasher.update("birthdays");
hasher.update(addressbook.push_topic);
format!("{:x}", hasher.finalize())
},
components: vec![CalendarObjectType::Event],
}
}
/// Objects are all prefixed with `BIRTHDAYS_PREFIX`
#[async_trait]
impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
let addressbook = self.0.get_addressbook(principal, id, show_deleted).await?;
Ok(birthday_calendar(addressbook))
}
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let addressbooks = self.0.get_addressbooks(principal).await?;
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
}
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let addressbooks = self.0.get_deleted_addressbooks(principal).await?;
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
}
async fn update_calendar(
&self,
_principal: String,
_id: String,
_calendar: Calendar,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn delete_calendar(
&self,
_principal: &str,
_name: &str,
_use_trashbin: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn restore_calendar(&self, _principal: &str, _name: &str) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn import_calendar(
&self,
_calendar: Calendar,
_objects: Vec<CalendarObject>,
_merge_existing: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn sync_changes(
&self,
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (objects, deleted_objects, new_synctoken) =
self.0.sync_changes(principal, cal_id, synctoken).await?;
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
.iter()
.map(AddressObject::get_birthday_object)
.collect();
let objects = objects?.into_iter().flatten().collect();
Ok((objects, deleted_objects, new_synctoken))
}
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<crate::CollectionMetadata, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
self.0.addressbook_metadata(principal, cal_id).await
}
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
self.0
.get_objects(principal, cal_id)
.await?
.iter()
.map(AddressObject::get_significant_dates)
.collect();
let objects = objects?
.into_iter()
.flat_map(HashMap::into_values)
.collect();
Ok(objects)
}
async fn get_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
self.0
.get_object(principal, cal_id, addressobject_id, show_deleted)
.await?
.get_significant_dates()?
.remove(date_type)
.ok_or(Error::NotFound)
}
async fn put_object(
&self,
_principal: String,
_cal_id: String,
_object: CalendarObject,
_overwrite: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn delete_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
_use_trashbin: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn restore_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
fn is_read_only(&self, _cal_id: &str) -> bool {
true
}
}

View File

@@ -7,7 +7,6 @@ pub use error::Error;
pub mod auth;
mod calendar;
mod combined_calendar_store;
mod contact_birthday_store;
mod secret;
mod subscription_store;
pub mod synctoken;
@@ -17,8 +16,7 @@ pub mod tests;
pub use addressbook_store::AddressbookStore;
pub use calendar_store::CalendarStore;
pub use combined_calendar_store::CombinedCalendarStore;
pub use contact_birthday_store::ContactBirthdayStore;
pub use combined_calendar_store::{CombinedCalendarStore, PrefixedCalendarStore};
pub use secret::Secret;
pub use subscription_store::*;

View File

@@ -30,3 +30,4 @@ uuid.workspace = true
pbkdf2.workspace = true
rustical_ical.workspace = true
rstest = { workspace = true, optional = true }
sha2.workspace = true

View File

@@ -0,0 +1 @@
DROP TABLE birthday_calendars;

View File

@@ -0,0 +1,26 @@
CREATE TABLE birthday_calendars (
principal TEXT NOT NULL,
id TEXT NOT NULL,
displayname TEXT,
description TEXT,
"order" INT DEFAULT 0 NOT NULL,
color TEXT,
timezone_id TEXT,
deleted_at DATETIME,
push_topic TEXT NOT NULL,
PRIMARY KEY (principal, id),
CONSTRAINT fk_birthdays_addressbooks FOREIGN KEY (principal, id)
REFERENCES addressbooks (principal, id) ON DELETE CASCADE
-- birthday calendar stores no meaningful data so we can cascade
);
INSERT INTO birthday_calendars
(principal, id, displayname, deleted_at, push_topic)
SELECT
principal,
id,
displayname || ' birthdays' AS displayname,
deleted_at,
push_topic || substr(printf('%d', random()), -4) AS push_topic
-- jank suffix to ensure that new push_topic is different :D
FROM addressbooks;

View File

@@ -0,0 +1,431 @@
use crate::addressbook_store::SqliteAddressbookStore;
use async_trait::async_trait;
use chrono::NaiveDateTime;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
use rustical_store::{
Addressbook, AddressbookStore, Calendar, CalendarMetadata, CalendarStore, CollectionMetadata,
Error, PrefixedCalendarStore,
};
use sha2::{Digest, Sha256};
use sqlx::{Executor, Sqlite};
use std::collections::HashMap;
use tracing::instrument;
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
struct BirthdayCalendarJoinRow {
principal: String,
id: String,
displayname: Option<String>,
description: Option<String>,
order: i64,
color: Option<String>,
timezone_id: Option<String>,
deleted_at: Option<NaiveDateTime>,
push_topic: String,
addr_synctoken: i64,
}
impl From<BirthdayCalendarJoinRow> for Calendar {
fn from(value: BirthdayCalendarJoinRow) -> Self {
Self {
principal: value.principal,
id: format!("{}{}", BIRTHDAYS_PREFIX, value.id),
meta: CalendarMetadata {
displayname: value.displayname,
order: value.order,
description: value.description,
color: value.color,
},
deleted_at: value.deleted_at,
components: vec![CalendarObjectType::Event],
timezone_id: value.timezone_id,
synctoken: value.addr_synctoken,
subscription_url: None,
push_topic: value.push_topic,
}
}
}
impl PrefixedCalendarStore for SqliteAddressbookStore {
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
}
impl SqliteAddressbookStore {
#[instrument]
pub async fn _get_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let cal = sqlx::query_as!(
BirthdayCalendarJoinRow,
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
FROM birthday_calendars
INNER JOIN (
SELECT principal AS addr_principal,
id AS addr_id,
synctoken AS addr_synctoken
FROM addressbooks
) ON (principal, id) = (addr_principal, addr_id)
WHERE (principal, id) = (?, ?)
AND ((deleted_at IS NULL) OR ?)
"#,
principal,
id,
show_deleted
)
.fetch_one(executor)
.await
.map_err(crate::Error::from)?;
Ok(cal.into())
}
#[instrument]
pub async fn _get_birthday_calendars<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
deleted: bool,
) -> Result<Vec<Calendar>, Error> {
Ok(
sqlx::query_as!(
BirthdayCalendarJoinRow,
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
FROM birthday_calendars
INNER JOIN (
SELECT principal AS addr_principal,
id AS addr_id,
synctoken AS addr_synctoken
FROM addressbooks
) ON (principal, id) = (addr_principal, addr_id)
WHERE principal = ?
AND (
(deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted
OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted
)
"#,
principal,
deleted,
deleted
)
.fetch_all(executor)
.await
.map_err(crate::Error::from).map(|cals| cals.into_iter().map(BirthdayCalendarJoinRow::into).collect())?)
}
#[must_use]
pub fn default_birthday_calendar(addressbook: Addressbook) -> Calendar {
let birthday_name = addressbook
.displayname
.as_ref()
.map(|name| format!("{name} birthdays"));
let birthday_push_topic = {
let mut hasher = Sha256::new();
hasher.update("birthdays");
hasher.update(&addressbook.push_topic);
format!("{:x}", hasher.finalize())
};
Calendar {
principal: addressbook.principal,
meta: CalendarMetadata {
displayname: birthday_name,
order: 0,
description: None,
color: None,
},
id: format!("{}{}", Self::PREFIX, addressbook.id),
components: vec![CalendarObjectType::Event],
timezone_id: None,
deleted_at: None,
synctoken: Default::default(),
subscription_url: None,
push_topic: birthday_push_topic,
}
}
#[instrument]
pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
calendar: &Calendar,
) -> Result<(), rustical_store::Error> {
let id = calendar
.id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
sqlx::query!(
r#"INSERT INTO birthday_calendars (principal, id, displayname, description, "order", color, push_topic)
VALUES (?, ?, ?, ?, ?, ?, ?)"#,
calendar.principal,
id,
calendar.meta.displayname,
calendar.meta.description,
calendar.meta.order,
calendar.meta.color,
calendar.push_topic,
)
.execute(executor)
.await
.map_err(crate::Error::from)?;
Ok(())
}
async fn _delete_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
if use_trashbin {
sqlx::query!(
r#"UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#,
principal,
id
)
.execute(executor)
.await
.map_err(crate::Error::from)?
} else {
sqlx::query!(
r#"DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)"#,
principal,
id
)
.execute(executor)
.await
.map_err(crate::Error::from)?
};
Ok(())
}
async fn _restore_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
) -> Result<(), Error> {
sqlx::query!(
r"UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
principal,
id
)
.execute(executor)
.await
.map_err(crate::Error::from)?;
Ok(())
}
#[instrument]
async fn _update_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
calendar: &Calendar,
) -> Result<(), Error> {
let result = sqlx::query!(
r#"UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone_id = ?, push_topic = ?
WHERE (principal, id) = (?, ?)"#,
calendar.principal,
calendar.id,
calendar.meta.displayname,
calendar.meta.description,
calendar.meta.order,
calendar.meta.color,
calendar.timezone_id,
calendar.push_topic,
principal,
calendar.id,
).execute(executor).await.map_err(crate::Error::from)?;
if result.rows_affected() == 0 {
return Err(rustical_store::Error::NotFound);
}
Ok(())
}
}
#[async_trait]
impl CalendarStore for SqliteAddressbookStore {
#[instrument]
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
Self::_get_birthday_calendar(&self.db, principal, id, show_deleted).await
}
#[instrument]
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
Self::_get_birthday_calendars(&self.db, principal, false).await
}
#[instrument]
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
Self::_get_birthday_calendars(&self.db, principal, true).await
}
#[instrument]
async fn update_calendar(
&self,
principal: String,
id: String,
mut calendar: Calendar,
) -> Result<(), Error> {
assert_eq!(id, calendar.id);
calendar.id = calendar
.id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?
.to_string();
Self::_update_birthday_calendar(&self.db, &principal, &calendar).await
}
#[instrument]
async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> {
Self::_insert_birthday_calendar(&self.db, &calendar).await
}
#[instrument]
async fn delete_calendar(
&self,
principal: &str,
id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
return Ok(());
};
Self::_delete_birthday_calendar(&self.db, principal, id, use_trashbin).await
}
#[instrument]
async fn restore_calendar(&self, principal: &str, id: &str) -> Result<(), Error> {
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
return Err(Error::NotFound);
};
Self::_restore_birthday_calendar(&self.db, principal, id).await
}
#[instrument]
async fn import_calendar(
&self,
_calendar: Calendar,
_objects: Vec<CalendarObject>,
_merge_existing: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn sync_changes(
&self,
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (objects, deleted_objects, new_synctoken) =
AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
.iter()
.map(AddressObject::get_birthday_object)
.collect();
let objects = objects?.into_iter().flatten().collect();
Ok((objects, deleted_objects, new_synctoken))
}
#[instrument]
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<CollectionMetadata, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
self.addressbook_metadata(principal, cal_id).await
}
#[instrument]
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
AddressbookStore::get_objects(self, principal, cal_id)
.await?
.iter()
.map(AddressObject::get_significant_dates)
.collect();
let objects = objects?
.into_iter()
.flat_map(HashMap::into_values)
.collect();
Ok(objects)
}
#[instrument]
async fn get_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
.await?
.get_significant_dates()?
.remove(date_type)
.ok_or(Error::NotFound)
}
#[instrument]
async fn put_object(
&self,
_principal: String,
_cal_id: String,
_object: CalendarObject,
_overwrite: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn delete_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
_use_trashbin: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn restore_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
fn is_read_only(&self, _cal_id: &str) -> bool {
true
}
}

View File

@@ -11,6 +11,8 @@ use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender;
use tracing::{error, instrument, warn};
pub mod birthday_calendar;
#[derive(Debug, Clone)]
struct AddressObjectRow {
id: String,
@@ -459,7 +461,16 @@ impl AddressbookStore for SqliteAddressbookStore {
&self,
addressbook: Addressbook,
) -> Result<(), rustical_store::Error> {
Self::_insert_addressbook(&self.db, &addressbook).await
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
let birthday_cal = Self::default_birthday_calendar(addressbook);
Self::_insert_birthday_calendar(&mut *tx, &birthday_cal).await?;
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
}
#[instrument]

View File

@@ -16,7 +16,8 @@ use rustical_frontend::{FrontendConfig, frontend_router};
use rustical_oidc::OidcConfig;
use rustical_store::auth::AuthenticationProvider;
use rustical_store::{
AddressbookStore, CalendarStore, CombinedCalendarStore, ContactBirthdayStore, SubscriptionStore,
AddressbookStore, CalendarStore, CombinedCalendarStore, PrefixedCalendarStore,
SubscriptionStore,
};
use std::sync::Arc;
use std::time::Duration;
@@ -33,7 +34,11 @@ use tracing::field::display;
clippy::too_many_lines,
clippy::cognitive_complexity
)]
pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
pub fn make_app<
AS: AddressbookStore + PrefixedCalendarStore,
CS: CalendarStore,
S: SubscriptionStore,
>(
addr_store: Arc<AS>,
cal_store: Arc<CS>,
subscription_store: Arc<S>,
@@ -45,7 +50,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
session_cookie_samesite_strict: bool,
payload_limit_mb: usize,
) -> Router<()> {
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store.clone()));
let birthday_store = addr_store.clone();
let combined_cal_store =
Arc::new(CombinedCalendarStore::new(cal_store).with_store(birthday_store));

View File

@@ -13,7 +13,9 @@ use figment::Figment;
use figment::providers::{Env, Format, Toml};
use rustical_dav_push::DavPushController;
use rustical_store::auth::AuthenticationProvider;
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
use rustical_store::{
AddressbookStore, CalendarStore, CollectionOperation, PrefixedCalendarStore, SubscriptionStore,
};
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
use rustical_store_sqlite::principal_store::SqlitePrincipalStore;
@@ -56,7 +58,7 @@ async fn get_data_stores(
migrate: bool,
config: &DataStoreConfig,
) -> Result<(
Arc<impl AddressbookStore>,
Arc<impl AddressbookStore + PrefixedCalendarStore>,
Arc<impl CalendarStore>,
Arc<impl SubscriptionStore>,
Arc<impl AuthenticationProvider>,