Compare commits

..

7 Commits

Author SHA1 Message Date
Lennart
c646986c56 Version 0.4.1 2025-06-23 14:08:06 +02:00
Lennart
503cbe3699 fix: Add default frontend config 2025-06-23 14:07:38 +02:00
Lennart
79c66a0b46 fix(caldav): Fix permissions to allow for deletion of calendar subscriptions
fixes #84
2025-06-23 14:04:09 +02:00
Lennart
e5687c6e43 fix(frontend): calendar subscription creation 2025-06-23 14:03:10 +02:00
Lennart
79b67a17c3 Implement deletion button to permanently delete collections 2025-06-23 13:48:00 +02:00
Lennart
7d18faff69 version 0.3.6 2025-06-23 11:21:04 +02:00
Lennart
753f8e90d3 fix(frontend): Fix calendar download link 2025-06-23 11:20:44 +02:00
23 changed files with 361 additions and 269 deletions

22
Cargo.lock generated
View File

@@ -2736,7 +2736,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -2779,7 +2779,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2814,7 +2814,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2846,7 +2846,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2871,7 +2871,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2897,7 +2897,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -2930,7 +2930,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -2948,7 +2948,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2963,7 +2963,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -2997,7 +2997,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.3.5" version = "0.4.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3017,7 +3017,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.3.5" version = "0.4.1"
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.3.5" version = "0.4.1"
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

@@ -292,7 +292,12 @@ impl Resource for CalendarResource {
} }
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
if self.cal.subscription_url.is_some() || self.read_only { if self.cal.subscription_url.is_some() {
return Ok(UserPrivilegeSet::owner_write_properties(
user.is_principal(&self.cal.principal),
));
}
if self.read_only {
return Ok(UserPrivilegeSet::owner_read( return Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.cal.principal), user.is_principal(&self.cal.principal),
)); ));

View File

@@ -2,6 +2,7 @@ use quick_xml::name::Namespace;
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
// https://datatracker.ietf.org/doc/html/rfc3744
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq)] #[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq)]
pub enum UserPrivilege { pub enum UserPrivilege {
Read, Read,
@@ -47,6 +48,12 @@ pub struct UserPrivilegeSet {
impl UserPrivilegeSet { impl UserPrivilegeSet {
pub fn has(&self, privilege: &UserPrivilege) -> bool { pub fn has(&self, privilege: &UserPrivilege) -> bool {
if (privilege == &UserPrivilege::WriteProperties
|| privilege == &UserPrivilege::WriteContent)
&& self.privileges.contains(&UserPrivilege::Write)
{
return true;
}
self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All) self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All)
} }
@@ -72,6 +79,15 @@ impl UserPrivilegeSet {
} }
} }
pub fn owner_write_properties(is_owner: bool) -> Self {
// Content is read-only but we can write properties
if is_owner {
Self::write_properties()
} else {
Self::default()
}
}
pub fn read_only() -> Self { pub fn read_only() -> Self {
Self { Self {
privileges: HashSet::from([ privileges: HashSet::from([
@@ -81,6 +97,17 @@ impl UserPrivilegeSet {
]), ]),
} }
} }
pub fn write_properties() -> Self {
Self {
privileges: HashSet::from([
UserPrivilege::Read,
UserPrivilege::WriteProperties,
UserPrivilege::ReadAcl,
UserPrivilege::ReadCurrentUserPrivilegeSet,
]),
}
}
} }
impl<const N: usize> From<[UserPrivilege; N]> for UserPrivilegeSet { impl<const N: usize> From<[UserPrivilege; N]> for UserPrivilegeSet {

View File

@@ -47,8 +47,9 @@ pub async fn route_delete<R: ResourceService>(
) -> Result<(), R::Error> { ) -> Result<(), R::Error> {
let resource = resource_service.get_resource(path_components).await?; let resource = resource_service.get_resource(path_components).await?;
// Kind of a bodge since we don't get unbind from the parent
let privileges = resource.get_user_privileges(principal)?; let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Write) { if !privileges.has(&UserPrivilege::WriteProperties) {
return Err(Error::Unauthorized.into()); return Err(Error::Unauthorized.into());
} }

View File

@@ -103,7 +103,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>${this.subscriptionUrl}</CS:source>` : ''} ${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

@@ -0,0 +1,44 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createClient } from "webdav";
@customElement("delete-button")
export class DeleteButton extends LitElement {
constructor() {
super()
}
@property({ type: Boolean })
trash: boolean = false
@property()
href: string
protected createRenderRoot() {
return this
}
protected render() {
let text = this.trash ? 'Move to trash' : 'Delete'
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
}
async _onClick(event: Event) {
event.preventDefault()
if (!this.trash && !confirm('Do you want to delete this collection permanently?')) {
return
}
let response = await fetch(this.href, {
method: 'DELETE',
headers: {
'X-No-Trashbin': this.trash ? '0' : '1'
}
})
if (response.status < 200 || response.status >= 300) {
alert('An error occured, look into the console')
console.error(response)
return
}
window.location.reload()
}
}

View File

@@ -15,6 +15,7 @@ export default defineConfig({
input: [ input: [
"lib/create-calendar-form.ts", "lib/create-calendar-form.ts",
"lib/create-addressbook-form.ts", "lib/create-addressbook-form.ts",
"lib/delete-button.ts",
], ],
output: { output: {
dir: "../public/assets/js/", dir: "../public/assets/js/",

View File

@@ -1,10 +1,11 @@
import { i as c, x as u } from "./lit-CWlWuEHk.mjs"; import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs"; import { n as o, t as h } from "./property-DYFkTqgI.mjs";
import { e as d, n as m } from "./ref-nf9JiOyl.mjs";
import { a as b } from "./webdav-Bz4I5vNH.mjs"; import { a as b } from "./webdav-Bz4I5vNH.mjs";
var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, a = (t, s, l, r) => { var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, r = (t, a, n, s) => {
for (var e = r > 1 ? void 0 : r ? f(s, l) : s, n = t.length - 1, p; n >= 0; n--) for (var e = s > 1 ? void 0 : s ? f(a, n) : a, l = t.length - 1, p; l >= 0; l--)
(p = t[n]) && (e = (r ? p(s, l, e) : p(e)) || e); (p = t[l]) && (e = (s ? p(a, n, e) : p(e)) || e);
return r && e && y(s, l, e), e; return s && e && y(a, n, e), e;
}; };
let i = class extends c { let i = class extends c {
constructor() { constructor() {
@@ -65,19 +66,19 @@ let i = class extends c {
}), window.location.reload(), null; }), window.location.reload(), null;
} }
}; };
a([ r([
o() o()
], i.prototype, "user", 2); ], i.prototype, "user", 2);
a([ r([
o() o()
], i.prototype, "id", 2); ], i.prototype, "id", 2);
a([ r([
o() o()
], i.prototype, "displayname", 2); ], i.prototype, "displayname", 2);
a([ r([
o() o()
], i.prototype, "description", 2); ], i.prototype, "description", 2);
i = a([ i = r([
h("create-addressbook-form") h("create-addressbook-form")
], i); ], i);
export { export {

View File

@@ -1,14 +1,15 @@
import { i as u, x as c } from "./lit-CWlWuEHk.mjs"; import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs"; import { n as o, t as h } from "./property-DYFkTqgI.mjs";
import { e as m, n as d } from "./ref-nf9JiOyl.mjs";
import { a as b } from "./webdav-Bz4I5vNH.mjs"; import { a as b } from "./webdav-Bz4I5vNH.mjs";
var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, l, s) => { var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, n, s) => {
for (var i = s > 1 ? void 0 : s ? $(e, l) : e, n = t.length - 1, p; n >= 0; n--) for (var i = s > 1 ? void 0 : s ? $(e, n) : e, l = t.length - 1, p; l >= 0; l--)
(p = t[n]) && (i = (s ? p(e, l, i) : p(i)) || i); (p = t[l]) && (i = (s ? p(e, n, i) : p(i)) || i);
return s && i && y(e, l, i), i; return s && i && y(e, n, i), i;
}; };
let r = class extends u { let r = class extends u {
constructor() { constructor() {
super(), this.client = b("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(), this.dialog = d(), this.form = d(); super(), this.client = b("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(), this.dialog = m(), this.form = m();
} }
createRenderRoot() { createRenderRoot() {
return this; return this;
@@ -16,9 +17,9 @@ let r = class extends u {
render() { render() {
return c` return c`
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button> <button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${m(this.dialog)}> <dialog ${d(this.dialog)}>
<h3>Create calendar</h3> <h3>Create calendar</h3>
<form @submit=${this.submit} ${m(this.form)}> <form @submit=${this.submit} ${d(this.form)}>
<label> <label>
id id
<input type="text" name="id" @change=${(t) => this.id = t.target.value} /> <input type="text" name="id" @change=${(t) => this.id = t.target.value} />
@@ -80,7 +81,7 @@ let r = class extends u {
<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>${this.subscriptionUrl}</CS:source>` : ""} ${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((e) => `<CAL:comp name="${e}" />`).join(` ${Array.from(this.components.keys()).map((e) => `<CAL:comp name="${e}" />`).join(`
`)} `)}

View File

@@ -0,0 +1,46 @@
import { i as c, x as p } from "./lit-CWlWuEHk.mjs";
import { n as h, t as u } from "./property-DYFkTqgI.mjs";
var f = Object.defineProperty, d = Object.getOwnPropertyDescriptor, i = (r, t, n, o) => {
for (var e = o > 1 ? void 0 : o ? d(t, n) : t, l = r.length - 1, a; l >= 0; l--)
(a = r[l]) && (e = (o ? a(t, n, e) : a(e)) || e);
return o && e && f(t, n, e), e;
};
let s = class extends c {
constructor() {
super(), this.trash = !1;
}
createRenderRoot() {
return this;
}
render() {
let r = this.trash ? "Move to trash" : "Delete";
return p`<button class="delete" @click=${(t) => this._onClick(t)}>${r}</button>`;
}
async _onClick(r) {
if (r.preventDefault(), !this.trash && !confirm("Do you want to delete this collection permanently?"))
return;
let t = await fetch(this.href, {
method: "DELETE",
headers: {
"X-No-Trashbin": this.trash ? "0" : "1"
}
});
if (t.status < 200 || t.status >= 300) {
alert("An error occured, look into the console"), console.error(t);
return;
}
window.location.reload();
}
};
i([
h({ type: Boolean })
], s.prototype, "trash", 2);
i([
h()
], s.prototype, "href", 2);
s = i([
u("delete-button")
], s);
export {
s as DeleteButton
};

View File

@@ -0,0 +1,47 @@
import { f as d, u as l } from "./lit-CWlWuEHk.mjs";
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const f = (t) => (r, e) => {
e !== void 0 ? e.addInitializer(() => {
customElements.define(t, r);
}) : customElements.define(t, r);
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const p = { attribute: !0, type: String, converter: l, reflect: !1, hasChanged: d }, u = (t = p, r, e) => {
const { kind: i, metadata: a } = e;
let n = globalThis.litPropertyMetadata.get(a);
if (n === void 0 && globalThis.litPropertyMetadata.set(a, n = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), n.set(e.name, t), i === "accessor") {
const { name: o } = e;
return { set(s) {
const c = r.get.call(this);
r.set.call(this, s), this.requestUpdate(o, c, t);
}, init(s) {
return s !== void 0 && this.C(o, void 0, t, s), s;
} };
}
if (i === "setter") {
const { name: o } = e;
return function(s) {
const c = this[o];
r.call(this, s), this.requestUpdate(o, c, t);
};
}
throw Error("Unsupported decorator location: " + i);
};
function m(t) {
return (r, e) => typeof e == "object" ? u(t, r, e) : ((i, a, n) => {
const o = a.hasOwnProperty(n);
return a.constructor.createProperty(n, i), o ? Object.getOwnPropertyDescriptor(a, n) : void 0;
})(t, r, e);
}
export {
m as n,
f as t
};

View File

@@ -1,172 +0,0 @@
import { f, u as _, E as $ } from "./lit-CWlWuEHk.mjs";
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const T = (t) => (e, s) => {
s !== void 0 ? s.addInitializer(() => {
customElements.define(t, e);
}) : customElements.define(t, e);
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const A = { attribute: !0, type: String, converter: _, reflect: !1, hasChanged: f }, p = (t = A, e, s) => {
const { kind: i, metadata: n } = s;
let r = globalThis.litPropertyMetadata.get(n);
if (r === void 0 && globalThis.litPropertyMetadata.set(n, r = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), r.set(s.name, t), i === "accessor") {
const { name: o } = s;
return { set(h) {
const l = e.get.call(this);
e.set.call(this, h), this.requestUpdate(o, l, t);
}, init(h) {
return h !== void 0 && this.C(o, void 0, t, h), h;
} };
}
if (i === "setter") {
const { name: o } = s;
return function(h) {
const l = this[o];
e.call(this, h), this.requestUpdate(o, l, t);
};
}
throw Error("Unsupported decorator location: " + i);
};
function O(t) {
return (e, s) => typeof s == "object" ? p(t, e, s) : ((i, n, r) => {
const o = n.hasOwnProperty(r);
return n.constructor.createProperty(r, i), o ? Object.getOwnPropertyDescriptor(n, r) : void 0;
})(t, e, s);
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const v = (t) => t.strings === void 0;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const g = { CHILD: 2 }, C = (t) => (...e) => ({ _$litDirective$: t, values: e });
class m {
constructor(e) {
}
get _$AU() {
return this._$AM._$AU;
}
_$AT(e, s, i) {
this._$Ct = e, this._$AM = s, this._$Ci = i;
}
_$AS(e, s) {
return this.update(e, s);
}
update(e, s) {
return this.render(...s);
}
}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const c = (t, e) => {
var i;
const s = t._$AN;
if (s === void 0) return !1;
for (const n of s) (i = n._$AO) == null || i.call(n, e, !1), c(n, e);
return !0;
}, a = (t) => {
let e, s;
do {
if ((e = t._$AM) === void 0) break;
s = e._$AN, s.delete(t), t = e;
} while ((s == null ? void 0 : s.size) === 0);
}, u = (t) => {
for (let e; e = t._$AM; t = e) {
let s = e._$AN;
if (s === void 0) e._$AN = s = /* @__PURE__ */ new Set();
else if (s.has(t)) break;
s.add(t), M(e);
}
};
function y(t) {
this._$AN !== void 0 ? (a(this), this._$AM = t, u(this)) : this._$AM = t;
}
function G(t, e = !1, s = 0) {
const i = this._$AH, n = this._$AN;
if (n !== void 0 && n.size !== 0) if (e) if (Array.isArray(i)) for (let r = s; r < i.length; r++) c(i[r], !1), a(i[r]);
else i != null && (c(i, !1), a(i));
else c(this, t);
}
const M = (t) => {
t.type == g.CHILD && (t._$AP ?? (t._$AP = G), t._$AQ ?? (t._$AQ = y));
};
class b extends m {
constructor() {
super(...arguments), this._$AN = void 0;
}
_$AT(e, s, i) {
super._$AT(e, s, i), u(this), this.isConnected = e._$AU;
}
_$AO(e, s = !0) {
var i, n;
e !== this.isConnected && (this.isConnected = e, e ? (i = this.reconnected) == null || i.call(this) : (n = this.disconnected) == null || n.call(this)), s && (c(this, e), a(this));
}
setValue(e) {
if (v(this._$Ct)) this._$Ct._$AI(e, this);
else {
const s = [...this._$Ct._$AH];
s[this._$Ci] = e, this._$Ct._$AI(s, this, 0);
}
}
disconnected() {
}
reconnected() {
}
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const P = () => new w();
class w {
}
const d = /* @__PURE__ */ new WeakMap(), U = C(class extends b {
render(t) {
return $;
}
update(t, [e]) {
var i;
const s = e !== this.G;
return s && this.G !== void 0 && this.rt(void 0), (s || this.lt !== this.ct) && (this.G = e, this.ht = (i = t.options) == null ? void 0 : i.host, this.rt(this.ct = t.element)), $;
}
rt(t) {
if (this.isConnected || (t = void 0), typeof this.G == "function") {
const e = this.ht ?? globalThis;
let s = d.get(e);
s === void 0 && (s = /* @__PURE__ */ new WeakMap(), d.set(e, s)), s.get(this.G) !== void 0 && this.G.call(this.ht, void 0), s.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
} else this.G.value = t;
}
get lt() {
var t, e;
return typeof this.G == "function" ? (t = d.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (e = this.G) == null ? void 0 : e.value;
}
disconnected() {
this.lt === this.ct && this.rt(void 0);
}
reconnected() {
this.rt(this.ct);
}
});
export {
O as a,
P as e,
U as n,
T as t
};

View File

@@ -0,0 +1,128 @@
import { E as $ } from "./lit-CWlWuEHk.mjs";
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const d = (t) => t.strings === void 0;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const _ = { CHILD: 2 }, a = (t) => (...s) => ({ _$litDirective$: t, values: s });
class A {
constructor(s) {
}
get _$AU() {
return this._$AM._$AU;
}
_$AT(s, e, i) {
this._$Ct = s, this._$AM = e, this._$Ci = i;
}
_$AS(s, e) {
return this.update(s, e);
}
update(s, e) {
return this.render(...e);
}
}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const n = (t, s) => {
var i;
const e = t._$AN;
if (e === void 0) return !1;
for (const h of e) (i = h._$AO) == null || i.call(h, s, !1), n(h, s);
return !0;
}, r = (t) => {
let s, e;
do {
if ((s = t._$AM) === void 0) break;
e = s._$AN, e.delete(t), t = s;
} while ((e == null ? void 0 : e.size) === 0);
}, l = (t) => {
for (let s; s = t._$AM; t = s) {
let e = s._$AN;
if (e === void 0) s._$AN = e = /* @__PURE__ */ new Set();
else if (e.has(t)) break;
e.add(t), v(s);
}
};
function f(t) {
this._$AN !== void 0 ? (r(this), this._$AM = t, l(this)) : this._$AM = t;
}
function u(t, s = !1, e = 0) {
const i = this._$AH, h = this._$AN;
if (h !== void 0 && h.size !== 0) if (s) if (Array.isArray(i)) for (let o = e; o < i.length; o++) n(i[o], !1), r(i[o]);
else i != null && (n(i, !1), r(i));
else n(this, t);
}
const v = (t) => {
t.type == _.CHILD && (t._$AP ?? (t._$AP = u), t._$AQ ?? (t._$AQ = f));
};
class p extends A {
constructor() {
super(...arguments), this._$AN = void 0;
}
_$AT(s, e, i) {
super._$AT(s, e, i), l(this), this.isConnected = s._$AU;
}
_$AO(s, e = !0) {
var i, h;
s !== this.isConnected && (this.isConnected = s, s ? (i = this.reconnected) == null || i.call(this) : (h = this.disconnected) == null || h.call(this)), e && (n(this, s), r(this));
}
setValue(s) {
if (d(this._$Ct)) this._$Ct._$AI(s, this);
else {
const e = [...this._$Ct._$AH];
e[this._$Ci] = s, this._$Ct._$AI(e, this, 0);
}
}
disconnected() {
}
reconnected() {
}
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const g = () => new C();
class C {
}
const c = /* @__PURE__ */ new WeakMap(), M = a(class extends p {
render(t) {
return $;
}
update(t, [s]) {
var i;
const e = s !== this.G;
return e && this.G !== void 0 && this.rt(void 0), (e || this.lt !== this.ct) && (this.G = s, this.ht = (i = t.options) == null ? void 0 : i.host, this.rt(this.ct = t.element)), $;
}
rt(t) {
if (this.isConnected || (t = void 0), typeof this.G == "function") {
const s = this.ht ?? globalThis;
let e = c.get(s);
e === void 0 && (e = /* @__PURE__ */ new WeakMap(), c.set(s, e)), e.get(this.G) !== void 0 && this.G.call(this.ht, void 0), e.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
} else this.G.value = t;
}
get lt() {
var t, s;
return typeof this.G == "function" ? (t = c.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (s = this.G) == null ? void 0 : s.value;
}
disconnected() {
this.lt === this.ct && this.rt(void 0);
}
reconnected() {
this.rt(this.ct);
}
});
export {
g as e,
M as n
};

View File

@@ -160,12 +160,12 @@ table {
display: grid; display: grid;
min-height: 80px; min-height: 80px;
grid-template-areas: grid-template-areas:
". . color-chip" ". . color-chip"
"title comps color-chip" "title comps color-chip"
"description . color-chip" "description description color-chip"
"subscription-url . color-chip" "subscription-url subscription-url color-chip"
"actions . color-chip" "actions actions color-chip"
". . color-chip"; ". . color-chip";
grid-template-rows: 12px auto auto auto auto 12px; grid-template-rows: 12px auto auto auto auto 12px;
grid-template-columns: min-content auto 80px; grid-template-columns: min-content auto 80px;
color: inherit; color: inherit;
@@ -220,6 +220,8 @@ table {
.actions { .actions {
grid-area: actions; grid-area: actions;
width: fit-content; width: fit-content;
display: flex;
gap: 12px;
} }
&:hover { &:hover {

View File

@@ -10,12 +10,4 @@
<pre>{{ addressbook|json }}</pre> <pre>{{ addressbook|json }}</pre>
<h2>Delete</h2>
<section>
<form method="POST" action="/frontend/user/{{addressbook.principal}}/addressbook/{{addressbook.id}}/delete">
<button type="submit">Move to trash</button>
</form>
</section>
{% endblock %} {% endblock %}

View File

@@ -31,12 +31,4 @@
<pre>{{ calendar|json }}</pre> <pre>{{ calendar|json }}</pre>
<h2>Delete</h2>
<section>
<form method="POST" action="/frontend/user/{{calendar.principal}}/calendar/{{calendar.id}}/delete">
<button type="submit">Move to trash</button>
</form>
</section>
{%endblock %} {%endblock %}

View File

@@ -3,6 +3,7 @@
{% block imports %} {% block imports %}
<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>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -83,9 +84,10 @@
<span class="subscription-url">{{ subscription_url }}</span> <span class="subscription-url">{{ subscription_url }}</span>
{% endif %} {% endif %}
<div class="actions"> <div class="actions">
<form action="/caldav/principal/{{ calendar.principal }}/calendar/{{ calendar.id }}" target="_blank" method="GET"> <form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
<button type="submit">Download</button> <button type="submit">Download</button>
</form> </form>
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
</div> </div>
<div class="color-chip"></div> <div class="color-chip"></div>
</a> </a>
@@ -114,6 +116,7 @@
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form"> <form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button> <button type="submit">Restore</button>
</form> </form>
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
</div> </div>
<div class="color-chip"></div> <div class="color-chip"></div>
</a> </a>
@@ -139,6 +142,7 @@
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET"> <form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET">
<button type="submit">Download</button> <button type="submit">Download</button>
</form> </form>
<delete-button trash href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
</div> </div>
</a> </a>
</li> </li>
@@ -160,6 +164,7 @@
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form"> <form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button> <button type="submit">Restore</button>
</form> </form>
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
</div> </div>
</a> </a>
</li> </li>

View File

@@ -12,3 +12,12 @@ pub struct FrontendConfig {
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub allow_password_login: bool, pub allow_password_login: bool,
} }
impl Default for FrontendConfig {
fn default() -> Self {
Self {
enabled: true,
allow_password_login: true,
}
}
}

View File

@@ -26,9 +26,9 @@ pub use config::FrontendConfig;
use oidc_user_store::OidcUserStore; use oidc_user_store::OidcUserStore;
use crate::routes::{ use crate::routes::{
addressbook::{route_addressbook, route_addressbook_restore, route_delete_addressbook}, addressbook::{route_addressbook, route_addressbook_restore},
app_token::{route_delete_app_token, route_post_app_token}, app_token::{route_delete_app_token, route_post_app_token},
calendar::{route_calendar, route_calendar_restore, route_delete_calendar}, calendar::{route_calendar, route_calendar_restore},
login::{route_get_login, route_post_login, route_post_logout}, login::{route_get_login, route_post_login, route_post_logout},
user::{route_get_home, route_root, route_user_named}, user::{route_get_home, route_root, route_user_named},
}; };
@@ -60,10 +60,6 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
"/user/{user}/calendar/{calendar}", "/user/{user}/calendar/{calendar}",
get(route_calendar::<CS>), get(route_calendar::<CS>),
) )
.route(
"/user/{user}/calendar/{calendar}/delete",
post(route_delete_calendar::<CS>),
)
.route( .route(
"/user/{user}/calendar/{calendar}/restore", "/user/{user}/calendar/{calendar}/restore",
post(route_calendar_restore::<CS>), post(route_calendar_restore::<CS>),
@@ -73,10 +69,6 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
"/user/{user}/addressbook/{addressbook}", "/user/{user}/addressbook/{addressbook}",
get(route_addressbook::<AS>), get(route_addressbook::<AS>),
) )
.route(
"/user/{user}/addressbook/{addressbook}/delete",
post(route_delete_addressbook::<AS>),
)
.route( .route(
"/user/{user}/addressbook/{addressbook}/restore", "/user/{user}/addressbook/{addressbook}/restore",
post(route_addressbook_restore::<AS>), post(route_addressbook_restore::<AS>),

View File

@@ -47,19 +47,3 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
None => (StatusCode::CREATED, "Restored").into_response(), None => (StatusCode::CREATED, "Restored").into_response(),
}) })
} }
pub async fn route_delete_addressbook<AS: AddressbookStore>(
Path((owner, addressbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>,
user: Principal,
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
store
.delete_addressbook(&owner, &addressbook_id, true)
.await?;
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
}

View File

@@ -47,17 +47,3 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
None => (StatusCode::CREATED, "Restored").into_response(), None => (StatusCode::CREATED, "Restored").into_response(),
}) })
} }
pub async fn route_delete_calendar<C: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<C>>,
user: Principal,
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
store.delete_calendar(&owner, &cal_id, true).await?;
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
}

View File

@@ -79,6 +79,7 @@ pub struct Config {
pub data_store: DataStoreConfig, pub data_store: DataStoreConfig,
#[serde(default)] #[serde(default)]
pub http: HttpConfig, pub http: HttpConfig,
#[serde(default)]
pub frontend: FrontendConfig, pub frontend: FrontendConfig,
#[serde(default)] #[serde(default)]
pub oidc: Option<OidcConfig>, pub oidc: Option<OidcConfig>,