diff --git a/.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json b/.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json new file mode 100644 index 0000000..9298694 --- /dev/null +++ b/.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json @@ -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" +} diff --git a/.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json b/.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json new file mode 100644 index 0000000..794b59e --- /dev/null +++ b/.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json @@ -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" +} diff --git a/.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json b/.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json new file mode 100644 index 0000000..e738181 --- /dev/null +++ b/.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json @@ -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" +} diff --git a/.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json b/.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json new file mode 100644 index 0000000..25390d9 --- /dev/null +++ b/.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json @@ -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" +} diff --git a/.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json b/.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json new file mode 100644 index 0000000..52b6314 --- /dev/null +++ b/.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json @@ -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" +} diff --git a/.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json b/.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json new file mode 100644 index 0000000..e08aeb3 --- /dev/null +++ b/.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json @@ -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" +} diff --git a/.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json b/.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json new file mode 100644 index 0000000..442774a --- /dev/null +++ b/.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a" +} diff --git a/Cargo.lock b/Cargo.lock index 225d5ae..3e988c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3327,6 +3327,7 @@ dependencies = [ "rustical_ical", "rustical_store", "serde", + "sha2", "sqlx", "thiserror 2.0.17", "tokio", diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index 2c891eb..65d5912 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -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 { - 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), diff --git a/crates/caldav/src/calendar/test_files/propfind.outputs b/crates/caldav/src/calendar/test_files/propfind.outputs index 9330cdf..9a28b63 100644 --- a/crates/caldav/src/calendar/test_files/propfind.outputs +++ b/crates/caldav/src/calendar/test_files/propfind.outputs @@ -211,6 +211,9 @@ END:VCALENDAR + + + diff --git a/crates/caldav/src/calendar/tests.rs b/crates/caldav/src/calendar/tests.rs index cebcdc0..cb6c8c5 100644 --- a/crates/caldav/src/calendar/tests.rs +++ b/crates/caldav/src/calendar/tests.rs @@ -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); } } } diff --git a/crates/dav/src/resource/methods/proppatch.rs b/crates/dav/src/resource/methods/proppatch.rs index dc24821..cbeb8b0 100644 --- a/crates/dav/src/resource/methods/proppatch.rs +++ b/crates/dav/src/resource/methods/proppatch.rs @@ -88,7 +88,7 @@ pub async fn route_proppatch( .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()); } diff --git a/crates/frontend/js-components/lib/create-birthday-calendar-form.ts b/crates/frontend/js-components/lib/create-birthday-calendar-form.ts new file mode 100644 index 0000000..a99ec9a --- /dev/null +++ b/crates/frontend/js-components/lib/create-birthday-calendar-form.ts @@ -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 = createRef() + form: Ref = createRef() + @property() + timezones: Array = [] + + override render() { + return html` + + +

Create calendar

+
+ +
+ +
+ +
+ + +
+
+ ` + } + + 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: ` + + + + ${escapeXml(this.displayname)} + ${this.description ? `${escapeXml(this.description)}` : ''} + ${this.color ? `${escapeXml(this.color)}` : ''} + + + + + + + ` + }) + + 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 + } +} diff --git a/crates/frontend/js-components/vite.config.ts b/crates/frontend/js-components/vite.config.ts index 7480569..9397a06 100644 --- a/crates/frontend/js-components/vite.config.ts +++ b/crates/frontend/js-components/vite.config.ts @@ -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", diff --git a/crates/frontend/public/assets/js/create-birthday-calendar-form.mjs b/crates/frontend/public/assets/js/create-birthday-calendar-form.mjs new file mode 100644 index 0000000..7978fc8 --- /dev/null +++ b/crates/frontend/public/assets/js/create-birthday-calendar-form.mjs @@ -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` + + +

Create calendar

+
+ +
+ +
+ +
+ + +
+
+ `; + } + 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: ` + + + + ${escapeXml(this.displayname)} + ${this.description ? `${escapeXml(this.description)}` : ""} + ${this.color ? `${escapeXml(this.color)}` : ""} + + + + + + + ` + }); + 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 +}; diff --git a/crates/frontend/public/assets/style.css b/crates/frontend/public/assets/style.css index b5083d4..d846006 100644 --- a/crates/frontend/public/assets/style.css +++ b/crates/frontend/public/assets/style.css @@ -285,6 +285,7 @@ ul.collection-list { grid-area: actions; width: fit-content; display: flex; + flex-wrap: wrap; gap: 12px; } } diff --git a/crates/frontend/public/templates/components/sections/addressbooks_section.html b/crates/frontend/public/templates/components/sections/addressbooks_section.html index e699b0b..cf2291c 100644 --- a/crates/frontend/public/templates/components/sections/addressbooks_section.html +++ b/crates/frontend/public/templates/components/sections/addressbooks_section.html @@ -1,6 +1,6 @@

{{user.id }}'s Addressbooks

    - {% for (meta, addressbook) in addressbooks %} + {% for (meta, birthday_cal, addressbook) in addressbooks %}
  • @@ -24,9 +24,17 @@ > + {% if !birthday_cal.is_some() %} + + {% endif %}
  • @@ -37,7 +45,7 @@ {%if !deleted_addressbooks.is_empty() %}

    Deleted Addressbooks

      - {% for (meta, addressbook) in deleted_addressbooks %} + {% for (meta, birthday_cal, addressbook) in deleted_addressbooks %}
    • diff --git a/crates/frontend/public/templates/components/sections/calendars_section.html b/crates/frontend/public/templates/components/sections/calendars_section.html index 67e5ccb..4adb036 100644 --- a/crates/frontend/public/templates/components/sections/calendars_section.html +++ b/crates/frontend/public/templates/components/sections/calendars_section.html @@ -24,7 +24,6 @@
      - {% if !calendar.id.starts_with("_birthdays_") %} - {% endif %}