mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caf10912e5 | ||
|
|
ec89cd6fa5 | ||
|
|
ae20573670 | ||
|
|
71cee2d20c | ||
|
|
83c6bf247e | ||
|
|
6bcc03d659 | ||
|
|
32f5c01716 | ||
|
|
40938cba02 | ||
|
|
a5663bf006 | ||
|
|
26306fd661 | ||
|
|
d8e4bd1cc4 | ||
|
|
a18ff2b400 | ||
|
|
bf13d95b97 |
26
.sqlx/query-660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474.json
generated
Normal file
26
.sqlx/query-660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474.json
generated
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT length(vcf) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM addressobjects WHERE principal = ? AND addressbook_id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "length!: u64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted!: bool",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Datetime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474"
|
||||||
|
}
|
||||||
26
.sqlx/query-d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48.json
generated
Normal file
26
.sqlx/query-d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48.json
generated
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT length(ics) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM calendarobjects WHERE principal = ? AND cal_id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "length!: u64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted!: bool",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Datetime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48"
|
||||||
|
}
|
||||||
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -2999,7 +2999,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical"
|
name = "rustical"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -3042,7 +3042,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_caldav"
|
name = "rustical_caldav"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3080,7 +3080,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_carddav"
|
name = "rustical_carddav"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3112,7 +3112,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav"
|
name = "rustical_dav"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3137,7 +3137,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav_push"
|
name = "rustical_dav_push"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3163,7 +3163,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_frontend"
|
name = "rustical_frontend"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_web",
|
"askama_web",
|
||||||
@@ -3196,7 +3196,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_ical"
|
name = "rustical_ical"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3214,7 +3214,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_oidc"
|
name = "rustical_oidc"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3229,7 +3229,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store"
|
name = "rustical_store"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3263,7 +3263,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store_sqlite"
|
name = "rustical_store_sqlite"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3284,7 +3284,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_xml"
|
name = "rustical_xml"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
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"
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ pub enum UserPrivilege {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl XmlSerialize for UserPrivilegeSet {
|
impl XmlSerialize for UserPrivilegeSet {
|
||||||
fn serialize<W: std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
#[derive(XmlSerialize)]
|
#[derive(XmlSerialize)]
|
||||||
pub struct FakeUserPrivilegeSet {
|
pub struct FakeUserPrivilegeSet {
|
||||||
@@ -35,7 +35,6 @@ impl XmlSerialize for UserPrivilegeSet {
|
|||||||
.serialize(ns, tag, namespaces, writer)
|
.serialize(ns, tag, namespaces, writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(refining_impl_trait)]
|
|
||||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
|
|||||||
|
|
||||||
let mut member_responses = Vec::new();
|
let mut member_responses = Vec::new();
|
||||||
if depth != &Depth::Zero {
|
if depth != &Depth::Zero {
|
||||||
|
// TODO: authorization check for member resources
|
||||||
for member in resource_service.get_members(path_components).await? {
|
for member in resource_service.get_members(path_components).await? {
|
||||||
member_responses.push(member.propfind(
|
member_responses.push(member.propfind(
|
||||||
&format!("{}/{}", path.trim_end_matches('/'), member.get_name()),
|
&format!("{}/{}", path.trim_end_matches('/'), member.get_name()),
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ pub struct PropstatElement<PropType: XmlSerialize> {
|
|||||||
pub status: StatusCode,
|
pub status: StatusCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xml_serialize_status<W: ::std::io::Write>(
|
fn xml_serialize_status(
|
||||||
status: &StatusCode,
|
status: &StatusCode,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer)
|
XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer)
|
||||||
}
|
}
|
||||||
@@ -49,12 +49,12 @@ pub struct ResponseElement<PropstatType: XmlSerialize> {
|
|||||||
pub propstat: Vec<PropstatWrapper<PropstatType>>,
|
pub propstat: Vec<PropstatWrapper<PropstatType>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xml_serialize_optional_status<W: ::std::io::Write>(
|
fn xml_serialize_optional_status(
|
||||||
val: &Option<StatusCode>,
|
val: &Option<StatusCode>,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
XmlSerialize::serialize(
|
XmlSerialize::serialize(
|
||||||
&val.map(|status| format!("HTTP/1.1 {}", status)),
|
&val.map(|status| format!("HTTP/1.1 {}", status)),
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ use std::collections::HashMap;
|
|||||||
pub struct TagList(Vec<(Option<NamespaceOwned>, String)>);
|
pub struct TagList(Vec<(Option<NamespaceOwned>, String)>);
|
||||||
|
|
||||||
impl XmlSerialize for TagList {
|
impl XmlSerialize for TagList {
|
||||||
fn serialize<W: std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
let prefix = ns
|
let prefix = ns
|
||||||
.map(|ns| namespaces.get(&ns))
|
.map(|ns| namespaces.get(&ns))
|
||||||
@@ -57,7 +57,6 @@ impl XmlSerialize for TagList {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(refining_impl_trait)]
|
|
||||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
Support ${comp}
|
Support ${comp}
|
||||||
<input type="checkbox" value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
<input type="checkbox" value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
|
<br>
|
||||||
`)}
|
`)}
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ let CreateCalendarForm = class extends i {
|
|||||||
Support ${comp}
|
Support ${comp}
|
||||||
<input type="checkbox" value=${comp} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
<input type="checkbox" value=${comp} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
|
<br>
|
||||||
`)}
|
`)}
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
|
|||||||
@@ -1,21 +1,41 @@
|
|||||||
:root {
|
:root {
|
||||||
--background-color: #FFF;
|
--background-color: #FFF;
|
||||||
--background-darker: #EEE;
|
--background-darker: #EEE;
|
||||||
|
--text-on-background-color: #111;
|
||||||
--primary-color: #2F2FE1;
|
--primary-color: #2F2FE1;
|
||||||
--primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
|
--primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
|
||||||
--text-on-primary-color: #FFF;
|
--text-on-primary-color: #FFF;
|
||||||
/* --color-red: #FE2060; */
|
/* --color-red: #FE2060; */
|
||||||
/* --color-red: #EE1D59; */
|
/* --color-red: #EE1D59; */
|
||||||
--color-red: #E31B39;
|
--color-red: #E31B39;
|
||||||
|
--dilute-color: black;
|
||||||
|
--border-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-color: #222;
|
||||||
|
--background-darker: #292929;
|
||||||
|
--text-on-background-color: #CACACA;
|
||||||
|
--primary-color: color-mix(in srgb, #2F2FE1, white 15%);
|
||||||
|
--primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
|
||||||
|
--text-on-primary-color: #FFF;
|
||||||
|
/* --color-red: #FE2060; */
|
||||||
|
--color-red: #EE1D59;
|
||||||
|
--dilute-color: white;
|
||||||
|
--border-color: color-mix(in srgb, var(--background-color), var(--dilute-color) 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
dialog {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-on-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
/* position: relative; */
|
/* position: relative; */
|
||||||
font-family: sans-serif;
|
font-family: 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
@@ -29,32 +49,65 @@ body {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--text-on-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background: var(--background-darker);
|
background: var(--background-darker);
|
||||||
height: 60px;
|
min-height: 60px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px;
|
padding: 4px 12px;
|
||||||
|
|
||||||
border: 2px solid black;
|
border: 2px solid var(--border-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
box-shadow: 4px 2px 12px -5px black;
|
box-shadow: 4px 2px 12px -5px black;
|
||||||
|
|
||||||
a {
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
a.logo {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 5%);
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 4px 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 2%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.icon {
|
||||||
|
width: 1.3em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.logout_form {
|
.logout_form {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
|
||||||
button {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +122,7 @@ button,
|
|||||||
.button {
|
.button {
|
||||||
border: none;
|
border: none;
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
padding: 10px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--text-on-primary-color);
|
color: var(--text-on-primary-color);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
@@ -97,7 +150,7 @@ input[type="password"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
border: 1px solid black;
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 4px 2px 12px -8px black;
|
box-shadow: 4px 2px 12px -8px black;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -108,7 +161,7 @@ section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border: 1px solid black;
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 4px 2px 12px -6px black;
|
box-shadow: 4px 2px 12px -6px black;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -118,7 +171,7 @@ table {
|
|||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid black;
|
border: 1px solid var(--border-color);
|
||||||
width: max-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,12 +179,8 @@ table {
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tr:nth-of-type(2n+1) { */
|
|
||||||
/* background: var(--background-darker); */
|
|
||||||
/* } */
|
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background: #DDD;
|
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:first-child th {
|
tr:first-child th {
|
||||||
@@ -151,87 +200,92 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#page-user {
|
ul.collection-list {
|
||||||
ul {
|
padding-left: 0;
|
||||||
padding-left: 0;
|
|
||||||
|
|
||||||
li.collection-list-item {
|
li.collection-list-item {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: contents;
|
display: contents;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
background: #EEE;
|
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 5%);
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
grid-template-areas:
|
height: fit-content;
|
||||||
". . color-chip"
|
grid-template-areas:
|
||||||
"title comps color-chip"
|
". . color-chip"
|
||||||
"description description color-chip"
|
"title comps color-chip"
|
||||||
"subscription-url subscription-url color-chip"
|
"description description color-chip"
|
||||||
"actions actions color-chip"
|
"subscription-url subscription-url color-chip"
|
||||||
". . color-chip";
|
"metadata metadata color-chip"
|
||||||
grid-template-rows: 12px auto auto auto auto 12px;
|
"actions actions color-chip"
|
||||||
grid-template-columns: min-content auto 80px;
|
". . color-chip";
|
||||||
row-gap: 4px;
|
grid-template-rows: 12px auto auto auto auto auto 12px;
|
||||||
color: inherit;
|
grid-template-columns: min-content auto 80px;
|
||||||
text-decoration: none;
|
row-gap: 4px;
|
||||||
padding-left: 12px;
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
padding-left: 12px;
|
||||||
|
|
||||||
border: 2px solid black;
|
border: 2px solid var(--border-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
box-shadow: 4px 2px 12px -6px black;
|
box-shadow: 4px 2px 12px -6px black;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: 8px initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comps {
|
||||||
|
grid-area: comps;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin: 8px initial;
|
margin: 0 2px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--text-on-primary-color);
|
||||||
|
font-size: .8em;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.comps {
|
.description {
|
||||||
grid-area: comps;
|
grid-area: description;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
.metadata {
|
||||||
margin: 0 2px;
|
grid-area: metadata;
|
||||||
background: var(--primary-color);
|
white-space: nowrap;
|
||||||
color: var(--text-on-primary-color);
|
}
|
||||||
font-size: .8em;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
.subscription-url {
|
||||||
grid-area: description;
|
grid-area: subscription-url;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscription-url {
|
.color-chip {
|
||||||
grid-area: subscription-url;
|
background: var(--color);
|
||||||
white-space: nowrap;
|
grid-area: color-chip;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-chip {
|
.actions {
|
||||||
background: var(--color);
|
grid-area: actions;
|
||||||
grid-area: color-chip;
|
width: fit-content;
|
||||||
}
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
&:hover {
|
||||||
grid-area: actions;
|
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||||
width: fit-content;
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #DDD;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +296,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
border: 1px solid black;
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
}
|
}
|
||||||
@@ -252,6 +306,20 @@ footer {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
/* position: absolute; */
|
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"] {
|
||||||
|
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
padding: 6px 6px;
|
||||||
|
color: var(--text-on-background-color);
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.icon {
|
||||||
|
stroke-width: 2px;
|
||||||
|
color: var(--text-on-background-color);
|
||||||
|
stroke: var(--text-on-background-color);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
<section>
|
|
||||||
<h2>Addressbooks</h2>
|
|
||||||
<ul>
|
|
||||||
{% for addressbook in addressbooks %}
|
|
||||||
<li class="collection-list-item">
|
|
||||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
|
||||||
<span class="title">
|
|
||||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
|
||||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
|
||||||
</span>
|
|
||||||
<span class="description">
|
|
||||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
|
||||||
</span>
|
|
||||||
<div class="actions">
|
|
||||||
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank"
|
|
||||||
method="GET">
|
|
||||||
<button type="submit">Download</button>
|
|
||||||
</form>
|
|
||||||
<delete-button trash
|
|
||||||
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
You do not have any addressbooks yet
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{%if !deleted_addressbooks.is_empty() %}
|
|
||||||
<h3>Deleted Addressbooks</h3>
|
|
||||||
<ul>
|
|
||||||
{% for addressbook in deleted_addressbooks %}
|
|
||||||
<li class="collection-list-item">
|
|
||||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
|
||||||
<span class="title">
|
|
||||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
|
||||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
|
||||||
</span>
|
|
||||||
<span class="description">
|
|
||||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
|
||||||
</span>
|
|
||||||
<div class="actions">
|
|
||||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore"
|
|
||||||
method="POST" class="restore-form">
|
|
||||||
<button type="submit">Restore</button>
|
|
||||||
</form>
|
|
||||||
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<create-addressbook-form user="{{ user.id }}"></create-addressbook-form>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<section>
|
|
||||||
<h2>Calendars</h2>
|
|
||||||
<ul>
|
|
||||||
{% for calendar in calendars %}
|
|
||||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
|
||||||
<li class="collection-list-item" style="--color: {{ color }}">
|
|
||||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
|
|
||||||
<span class="title">
|
|
||||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
|
||||||
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
|
||||||
</span>
|
|
||||||
<div class="comps">
|
|
||||||
{% for comp in calendar.components %}
|
|
||||||
<span>{{ comp }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<span class="description">
|
|
||||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
|
||||||
</span>
|
|
||||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
|
||||||
<span class="subscription-url">{{ subscription_url }}</span>
|
|
||||||
{% endif %}
|
|
||||||
<div class="actions">
|
|
||||||
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
|
||||||
<button type="submit">Download</button>
|
|
||||||
</form>
|
|
||||||
{% if !calendar.id.starts_with("_birthdays_") %}
|
|
||||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="color-chip"></div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
You do not have any calendars yet
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{%if !deleted_calendars.is_empty() %}
|
|
||||||
<h3>Deleted Calendars</h3>
|
|
||||||
<ul>
|
|
||||||
{% for calendar in deleted_calendars %}
|
|
||||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
|
||||||
<li class="collection-list-item" style="--color: {{ color }}">
|
|
||||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
|
|
||||||
<span class="title">
|
|
||||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
|
||||||
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
|
||||||
</span>
|
|
||||||
<div class="comps">
|
|
||||||
{% for comp in calendar.components %}
|
|
||||||
<span>{{ comp }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<span class="description">
|
|
||||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
|
||||||
</span>
|
|
||||||
<div class="actions">
|
|
||||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
|
|
||||||
class="restore-form">
|
|
||||||
<button type="submit">Restore</button>
|
|
||||||
</form>
|
|
||||||
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
|
||||||
</div>
|
|
||||||
<div class="color-chip"></div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<section>
|
|
||||||
<h2>Profile</h2>
|
|
||||||
|
|
||||||
{% let groups = user.memberships_without_self() %}
|
|
||||||
{% if groups.len() > 0 %}
|
|
||||||
<h3>Groups</h3>
|
|
||||||
<ul>
|
|
||||||
{% for group in groups %}
|
|
||||||
<li>{{ group }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h3>App tokens</h3>
|
|
||||||
<table id="app-tokens">
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Created at</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
{% for app_token in app_tokens %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ app_token.name }}</td>
|
|
||||||
<td>
|
|
||||||
{% if let Some(created_at) = app_token.created_at %}
|
|
||||||
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<form action="/frontend/user/{{ user.id }}/app_token/{{ app_token.id }}/delete" method="POST">
|
|
||||||
<button type="submit" class="delete">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
<tr class="generate">
|
|
||||||
<td>
|
|
||||||
<form action="/frontend/user/{{ user.id }}/app_token" method="POST" id="form_generate_app_token">
|
|
||||||
<label class="font_bold" for="generate_app_token_name">App name</label>
|
|
||||||
<input type="text" name="name" id="generate_app_token_name" />
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<button type="submit" form="form_generate_app_token">Generate</button>
|
|
||||||
{% if is_apple %}
|
|
||||||
<button type="submit" form="form_generate_app_token" name="apple" value="true">Apple Configuration Profile
|
|
||||||
(contains token)</button>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{% if let Some(hostname) = davx5_hostname %}
|
|
||||||
<a
|
|
||||||
href="intent://{{ hostname | urlencode }}#Intent;action=android.intent.action.VIEW;component=at.bitfire.davdroid.ui.setup.LoginActivity;scheme=davx5;package=at.bitfire.davdroid;S.loginFlow=1;end">Configure
|
|
||||||
in DAVx5</a>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<h2>{{user.id }}'s Addressbooks</h2>
|
||||||
|
<ul class="collection-list">
|
||||||
|
{% for (meta, addressbook) in addressbooks %}
|
||||||
|
<li class="collection-list-item">
|
||||||
|
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||||
|
<span class="title">
|
||||||
|
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||||
|
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||||
|
</span>
|
||||||
|
<span class="description">
|
||||||
|
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||||
|
</span>
|
||||||
|
<div class="actions">
|
||||||
|
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank"
|
||||||
|
method="GET">
|
||||||
|
<button type="submit">Download</button>
|
||||||
|
</form>
|
||||||
|
<delete-button trash
|
||||||
|
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||||
|
</div>
|
||||||
|
<div class="metadata">
|
||||||
|
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
You do not have any addressbooks yet
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{%if !deleted_addressbooks.is_empty() %}
|
||||||
|
<h3>Deleted Addressbooks</h3>
|
||||||
|
<ul class="collection-list">
|
||||||
|
{% for (meta, addressbook) in deleted_addressbooks %}
|
||||||
|
<li class="collection-list-item">
|
||||||
|
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||||
|
<span class="title">
|
||||||
|
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||||
|
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||||
|
</span>
|
||||||
|
<span class="description">
|
||||||
|
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||||
|
</span>
|
||||||
|
<div class="actions">
|
||||||
|
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore"
|
||||||
|
method="POST" class="restore-form">
|
||||||
|
<button type="submit">Restore</button>
|
||||||
|
</form>
|
||||||
|
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||||
|
</div>
|
||||||
|
<div class="metadata">
|
||||||
|
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<create-addressbook-form user="{{ user.id }}"></create-addressbook-form>
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<h2>{{ user.id }}'s Calendars</h2>
|
||||||
|
<ul class="collection-list">
|
||||||
|
{% for (meta, calendar) in calendars %}
|
||||||
|
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||||
|
<li class="collection-list-item" style="--color: {{ color }}">
|
||||||
|
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
|
||||||
|
<span class="title">
|
||||||
|
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||||
|
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
||||||
|
</span>
|
||||||
|
<div class="comps">
|
||||||
|
{% for comp in calendar.components %}
|
||||||
|
<span>{{ comp }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<span class="description">
|
||||||
|
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||||
|
<span class="subscription-url">{{ subscription_url }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<div class="actions">
|
||||||
|
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
||||||
|
<button type="submit">Download</button>
|
||||||
|
</form>
|
||||||
|
{% if !calendar.id.starts_with("_birthdays_") %}
|
||||||
|
<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
|
||||||
|
</div>
|
||||||
|
<div class="color-chip"></div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
You do not have any calendars yet
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{%if !deleted_calendars.is_empty() %}
|
||||||
|
<h3>Deleted Calendars</h3>
|
||||||
|
<ul class="collection-list">
|
||||||
|
{% for (meta, calendar) in deleted_calendars %}
|
||||||
|
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||||
|
<li class="collection-list-item" style="--color: {{ color }}">
|
||||||
|
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
|
||||||
|
<span class="title">
|
||||||
|
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||||
|
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
||||||
|
</span>
|
||||||
|
<div class="comps">
|
||||||
|
{% for comp in calendar.components %}
|
||||||
|
<span>{{ comp }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<span class="description">
|
||||||
|
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||||
|
</span>
|
||||||
|
<div class="actions">
|
||||||
|
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
|
||||||
|
class="restore-form">
|
||||||
|
<button type="submit">Restore</button>
|
||||||
|
</form>
|
||||||
|
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||||
|
</div>
|
||||||
|
<div class="metadata">
|
||||||
|
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||||
|
</div>
|
||||||
|
<div class="color-chip"></div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<h2>{{ user.id }}'s Profile</h2>
|
||||||
|
|
||||||
|
{% let groups = user.memberships_without_self() %}
|
||||||
|
{% if groups.len() > 0 %}
|
||||||
|
<h3>Groups</h3>
|
||||||
|
<ul>
|
||||||
|
{% for group in groups %}
|
||||||
|
<li>{{ group }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h3>App tokens</h3>
|
||||||
|
<table id="app-tokens">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Created at</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
{% for app_token in app_tokens %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ app_token.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if let Some(created_at) = app_token.created_at %}
|
||||||
|
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form action="/frontend/user/{{ user.id }}/app_token/{{ app_token.id }}/delete" method="POST">
|
||||||
|
<button type="submit" class="delete">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr class="generate">
|
||||||
|
<td>
|
||||||
|
<form action="/frontend/user/{{ user.id }}/app_token" method="POST" id="form_generate_app_token">
|
||||||
|
<label class="font_bold" for="generate_app_token_name">App name</label>
|
||||||
|
<input type="text" name="name" id="generate_app_token_name" />
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<button type="submit" form="form_generate_app_token">Generate</button>
|
||||||
|
{% if is_apple %}
|
||||||
|
<button type="submit" form="form_generate_app_token" name="apple" value="true">Apple Configuration Profile
|
||||||
|
(contains token)</button>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% if let Some(hostname) = davx5_hostname %}
|
||||||
|
<a
|
||||||
|
href="intent://{{ hostname | urlencode }}#Intent;action=android.intent.action.VIEW;component=at.bitfire.davdroid.ui.setup.LoginActivity;scheme=davx5;package=at.bitfire.davdroid;S.loginFlow=1;end">Configure
|
||||||
|
in DAVx5</a>
|
||||||
|
{% endif %}
|
||||||
8
crates/frontend/public/templates/icons/calendar.svg
Normal file
8
crates/frontend/public/templates/icons/calendar.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!-- Adapted from https://iconoir.com/ -->
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
||||||
|
<path d="M15 4V2M15 4V6M15 4H10.5M3 10V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V10H3Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M3 10V6C3 4.89543 3.89543 4 5 4H7" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M7 2V6" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M21 10V6C21 4.89543 20.1046 4 19 4H18.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
8
crates/frontend/public/templates/icons/group.svg
Normal file
8
crates/frontend/public/templates/icons/group.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!-- Adapted from https://iconoir.com/ -->
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
||||||
|
<path d="M1 20V19C1 15.134 4.13401 12 8 12V12C11.866 12 15 15.134 15 19V20" stroke-linecap="round"></path>
|
||||||
|
<path d="M13 14V14C13 11.2386 15.2386 9 18 9V9C20.7614 9 23 11.2386 23 14V14.5" stroke-linecap="round"></path>
|
||||||
|
<path d="M8 12C10.2091 12 12 10.2091 12 8C12 5.79086 10.2091 4 8 4C5.79086 4 4 5.79086 4 8C4 10.2091 5.79086 12 8 12Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M18 9C19.6569 9 21 7.65685 21 6C21 4.34315 19.6569 3 18 3C16.3431 3 15 4.34315 15 6C15 7.65685 16.3431 9 18 9Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
6
crates/frontend/public/templates/icons/user.svg
Normal file
6
crates/frontend/public/templates/icons/user.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Adapted from https://iconoir.com/ -->
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
||||||
|
<path d="M5 20V19C5 15.134 8.13401 12 12 12V12C15.866 12 19 15.134 19 19V20" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M12 12C14.2091 12 16 10.2091 16 8C16 5.79086 14.2091 4 12 4C9.79086 4 8 5.79086 8 8C8 10.2091 9.79086 12 12 12Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<header>
|
<header>
|
||||||
<a href="/frontend/user">RustiCal</a>
|
<a class="logo" href="/frontend/user">RustiCal</a>
|
||||||
|
{% block header_center %}{% endblock %}
|
||||||
<form method="POST" action="/frontend/logout" class="logout_form">
|
<form method="POST" action="/frontend/logout" class="logout_form">
|
||||||
<button type="submit">Log out</button>
|
<button type="submit">Log out</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
|
||||||
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block header_center %}
|
||||||
{% block content %}
|
<nav class="header-center">
|
||||||
<div id="page-user">
|
<a href="/frontend/user/{{ user.id }}" {% if S::name() == "profile" %}class="active"{% endif %}>{% include "icons/user.svg" %}Profile</a>
|
||||||
|
<a href="/frontend/user/{{ user.id }}/calendar" {% if S::name() == "calendars" %}class="active"{% endif %}>{% include "icons/calendar.svg" %}Calendars</a>
|
||||||
<h1>Welcome {{ user.id }}!</h1>
|
<a href="/frontend/user/{{ user.id }}/addressbook" {% if S::name() == "addressbooks" %}class="active"{% endif %}>{% include "icons/group.svg" %}Addressbooks</a>
|
||||||
|
</nav>
|
||||||
{% include "components/profile_section.html" %}
|
{% endblock %}
|
||||||
{% include "components/calendars_section.html" %}
|
|
||||||
{% include "components/addressbooks_section.html" %}
|
{% block content %}
|
||||||
|
{{ section|safe }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use headers::{ContentType, HeaderMapExt};
|
use headers::{ContentType, HeaderMapExt};
|
||||||
use http::{Method, StatusCode};
|
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_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
AddressbookStore, CalendarStore,
|
AddressbookStore, CalendarStore,
|
||||||
@@ -20,6 +21,7 @@ mod assets;
|
|||||||
mod config;
|
mod config;
|
||||||
pub mod nextcloud_login;
|
pub mod nextcloud_login;
|
||||||
mod oidc_user_store;
|
mod oidc_user_store;
|
||||||
|
pub(crate) mod pages;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
pub use config::FrontendConfig;
|
pub use config::FrontendConfig;
|
||||||
@@ -56,6 +58,7 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
|||||||
post(route_delete_app_token::<AP>),
|
post(route_delete_app_token::<AP>),
|
||||||
)
|
)
|
||||||
// Calendar
|
// Calendar
|
||||||
|
.route("/user/{user}/calendar", get(route_calendars::<CS>))
|
||||||
.route(
|
.route(
|
||||||
"/user/{user}/calendar/{calendar}",
|
"/user/{user}/calendar/{calendar}",
|
||||||
get(route_calendar::<CS>),
|
get(route_calendar::<CS>),
|
||||||
@@ -65,6 +68,7 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
|||||||
post(route_calendar_restore::<CS>),
|
post(route_calendar_restore::<CS>),
|
||||||
)
|
)
|
||||||
// Addressbook
|
// Addressbook
|
||||||
|
.route("/user/{user}/addressbook", get(route_addressbooks::<AS>))
|
||||||
.route(
|
.route(
|
||||||
"/user/{user}/addressbook/{addressbook}",
|
"/user/{user}/addressbook/{addressbook}",
|
||||||
get(route_addressbook::<AS>),
|
get(route_addressbook::<AS>),
|
||||||
|
|||||||
1
crates/frontend/src/pages/mod.rs
Normal file
1
crates/frontend/src/pages/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod user;
|
||||||
14
crates/frontend/src/pages/user.rs
Normal file
14
crates/frontend/src/pages/user.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use askama::Template;
|
||||||
|
use askama_web::WebTemplate;
|
||||||
|
use rustical_store::auth::Principal;
|
||||||
|
|
||||||
|
pub trait Section: Template {
|
||||||
|
fn name() -> &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template, WebTemplate)]
|
||||||
|
#[template(path = "pages/user.html")]
|
||||||
|
pub struct UserPage<S: Section> {
|
||||||
|
pub user: Principal,
|
||||||
|
pub section: S,
|
||||||
|
}
|
||||||
75
crates/frontend/src/routes/addressbooks.rs
Normal file
75
crates/frontend/src/routes/addressbooks.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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 crate::pages::user::{Section, UserPage};
|
||||||
|
|
||||||
|
impl Section for AddressbooksSection {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"addressbooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template, WebTemplate)]
|
||||||
|
#[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 async fn route_addressbooks<AS: AddressbookStore>(
|
||||||
|
Path(user_id): Path<String>,
|
||||||
|
Extension(addr_store): Extension<Arc<AS>>,
|
||||||
|
user: Principal,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if user_id != user.id {
|
||||||
|
return StatusCode::UNAUTHORIZED.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut addressbooks = vec![];
|
||||||
|
for group in user.memberships() {
|
||||||
|
addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut deleted_addressbooks = vec![];
|
||||||
|
for group in user.memberships() {
|
||||||
|
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut addressbook_infos = vec![];
|
||||||
|
for addressbook in addressbooks {
|
||||||
|
addressbook_infos.push((
|
||||||
|
addr_store
|
||||||
|
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
addressbook,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut deleted_addressbook_infos = vec![];
|
||||||
|
for addressbook in deleted_addressbooks {
|
||||||
|
deleted_addressbook_infos.push((
|
||||||
|
addr_store
|
||||||
|
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
addressbook,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPage {
|
||||||
|
section: AddressbooksSection {
|
||||||
|
user: user.clone(),
|
||||||
|
addressbooks: addressbook_infos,
|
||||||
|
deleted_addressbooks: deleted_addressbook_infos,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
74
crates/frontend/src/routes/calendars.rs
Normal file
74
crates/frontend/src/routes/calendars.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::pages::user::{Section, UserPage};
|
||||||
|
use askama::Template;
|
||||||
|
use askama_web::WebTemplate;
|
||||||
|
use axum::{Extension, extract::Path, response::IntoResponse};
|
||||||
|
use http::StatusCode;
|
||||||
|
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, auth::Principal};
|
||||||
|
|
||||||
|
impl Section for CalendarsSection {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"calendars"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template, WebTemplate)]
|
||||||
|
#[template(path = "components/sections/calendars_section.html")]
|
||||||
|
pub struct CalendarsSection {
|
||||||
|
pub user: Principal,
|
||||||
|
pub calendars: Vec<(CollectionMetadata, Calendar)>,
|
||||||
|
pub deleted_calendars: Vec<(CollectionMetadata, Calendar)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn route_calendars<CS: CalendarStore>(
|
||||||
|
Path(user_id): Path<String>,
|
||||||
|
Extension(cal_store): Extension<Arc<CS>>,
|
||||||
|
user: Principal,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if user_id != user.id {
|
||||||
|
return StatusCode::UNAUTHORIZED.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut calendars = vec![];
|
||||||
|
for group in user.memberships() {
|
||||||
|
calendars.extend(cal_store.get_calendars(group).await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut calendar_infos = vec![];
|
||||||
|
for calendar in calendars {
|
||||||
|
calendar_infos.push((
|
||||||
|
cal_store
|
||||||
|
.calendar_metadata(&calendar.principal, &calendar.id)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
calendar,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut deleted_calendars = vec![];
|
||||||
|
for group in user.memberships() {
|
||||||
|
deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut deleted_calendar_infos = vec![];
|
||||||
|
for calendar in deleted_calendars {
|
||||||
|
deleted_calendar_infos.push((
|
||||||
|
cal_store
|
||||||
|
.calendar_metadata(&calendar.principal, &calendar.id)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
calendar,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPage {
|
||||||
|
section: CalendarsSection {
|
||||||
|
user: user.clone(),
|
||||||
|
calendars: calendar_infos,
|
||||||
|
deleted_calendars: deleted_calendar_infos,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
pub mod addressbook;
|
pub mod addressbook;
|
||||||
|
pub mod addressbooks;
|
||||||
pub mod app_token;
|
pub mod app_token;
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
|
pub mod calendars;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
@@ -11,19 +11,23 @@ use axum_extra::{TypedHeader, extract::Host};
|
|||||||
use headers::UserAgent;
|
use headers::UserAgent;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
AddressbookStore, CalendarStore,
|
||||||
auth::{AppToken, AuthenticationProvider, Principal},
|
auth::{AppToken, AuthenticationProvider, Principal},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::pages::user::{Section, UserPage};
|
||||||
|
|
||||||
|
impl Section for ProfileSection {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Template, WebTemplate)]
|
#[derive(Template, WebTemplate)]
|
||||||
#[template(path = "pages/user.html")]
|
#[template(path = "components/sections/profile_section.html")]
|
||||||
pub struct UserPage {
|
pub struct ProfileSection {
|
||||||
pub user: Principal,
|
pub user: Principal,
|
||||||
pub app_tokens: Vec<AppToken>,
|
pub app_tokens: Vec<AppToken>,
|
||||||
pub calendars: Vec<Calendar>,
|
|
||||||
pub deleted_calendars: Vec<Calendar>,
|
|
||||||
pub addressbooks: Vec<Addressbook>,
|
|
||||||
pub deleted_addressbooks: Vec<Addressbook>,
|
|
||||||
pub is_apple: bool,
|
pub is_apple: bool,
|
||||||
pub davx5_hostname: Option<String>,
|
pub davx5_hostname: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -69,14 +73,13 @@ pub async fn route_user_named<
|
|||||||
let davx5_hostname = user_agent.as_str().contains("Android").then_some(host);
|
let davx5_hostname = user_agent.as_str().contains("Android").then_some(host);
|
||||||
|
|
||||||
UserPage {
|
UserPage {
|
||||||
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
section: ProfileSection {
|
||||||
calendars,
|
user: user.clone(),
|
||||||
deleted_calendars,
|
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
||||||
addressbooks,
|
is_apple,
|
||||||
deleted_addressbooks,
|
davx5_hostname,
|
||||||
|
},
|
||||||
user,
|
user,
|
||||||
is_apple,
|
|
||||||
davx5_hostname,
|
|
||||||
}
|
}
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Error, addressbook::Addressbook};
|
use crate::{CollectionMetadata, Error, addressbook::Addressbook};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustical_ical::AddressObject;
|
use rustical_ical::AddressObject;
|
||||||
|
|
||||||
@@ -35,6 +35,12 @@ pub trait AddressbookStore: Send + Sync + 'static {
|
|||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
) -> Result<(Vec<AddressObject>, Vec<String>, i64), Error>;
|
) -> Result<(Vec<AddressObject>, Vec<String>, i64), Error>;
|
||||||
|
|
||||||
|
async fn addressbook_metadata(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
addressbook_id: &str,
|
||||||
|
) -> Result<CollectionMetadata, Error>;
|
||||||
|
|
||||||
async fn get_objects(
|
async fn get_objects(
|
||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Calendar, error::Error};
|
use crate::{Calendar, CollectionMetadata, error::Error};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use rustical_ical::CalendarObject;
|
use rustical_ical::CalendarObject;
|
||||||
@@ -53,6 +53,12 @@ pub trait CalendarStore: Send + Sync + 'static {
|
|||||||
self.get_objects(principal, cal_id).await
|
self.get_objects(principal, cal_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn calendar_metadata(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
) -> Result<CollectionMetadata, Error>;
|
||||||
|
|
||||||
async fn get_objects(
|
async fn get_objects(
|
||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
|
|||||||
@@ -135,6 +135,20 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
async fn calendar_metadata(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
) -> Result<crate::CollectionMetadata, Error> {
|
||||||
|
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
||||||
|
self.birthday_store
|
||||||
|
.calendar_metadata(principal, cal_id)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
self.cal_store.calendar_metadata(principal, cal_id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
#[inline]
|
#[inline]
|
||||||
async fn get_objects(
|
async fn get_objects(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
|
|||||||
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
|
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
|
||||||
displayname: addressbook
|
displayname: addressbook
|
||||||
.displayname
|
.displayname
|
||||||
.map(|name| format!("{} birthdays", name)),
|
.map(|name| format!("{name} birthdays")),
|
||||||
order: 0,
|
order: 0,
|
||||||
description: None,
|
description: None,
|
||||||
color: None,
|
color: None,
|
||||||
@@ -104,6 +104,17 @@ impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
|
|||||||
Ok((objects, deleted_objects, new_synctoken))
|
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(
|
async fn get_objects(
|
||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
|
|||||||
@@ -37,3 +37,11 @@ pub struct CollectionOperation {
|
|||||||
pub topic: String,
|
pub topic: String,
|
||||||
pub data: CollectionOperationInfo,
|
pub data: CollectionOperationInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct CollectionMetadata {
|
||||||
|
pub len: usize,
|
||||||
|
pub deleted_len: usize,
|
||||||
|
pub size: u64,
|
||||||
|
pub deleted_size: u64,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const SYNC_NAMESPACE: &str = "github.com/lennart-k/rustical/ns/";
|
const SYNC_NAMESPACE: &str = "github.com/lennart-k/rustical/ns/";
|
||||||
|
|
||||||
pub fn format_synctoken(synctoken: i64) -> String {
|
pub fn format_synctoken(synctoken: i64) -> String {
|
||||||
format!("{}{}", SYNC_NAMESPACE, synctoken)
|
format!("{SYNC_NAMESPACE}{synctoken}")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_synctoken(synctoken: &str) -> Option<i64> {
|
pub fn parse_synctoken(synctoken: &str) -> Option<i64> {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use async_trait::async_trait;
|
|||||||
use derive_more::derive::Constructor;
|
use derive_more::derive::Constructor;
|
||||||
use rustical_ical::AddressObject;
|
use rustical_ical::AddressObject;
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
Addressbook, AddressbookStore, CollectionOperation, CollectionOperationInfo, Error,
|
Addressbook, AddressbookStore, CollectionMetadata, CollectionOperation,
|
||||||
synctoken::format_synctoken,
|
CollectionOperationInfo, Error, synctoken::format_synctoken,
|
||||||
};
|
};
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
@@ -223,6 +223,28 @@ impl SqliteAddressbookStore {
|
|||||||
Ok((objects, deleted_objects, new_synctoken))
|
Ok((objects, deleted_objects, new_synctoken))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
|
executor: E,
|
||||||
|
principal: &str,
|
||||||
|
addressbook_id: &str,
|
||||||
|
) -> Result<Vec<(u64, bool)>, rustical_store::Error> {
|
||||||
|
struct ObjectEntry {
|
||||||
|
length: u64,
|
||||||
|
deleted: bool,
|
||||||
|
}
|
||||||
|
Ok(sqlx::query_as!(
|
||||||
|
ObjectEntry,
|
||||||
|
"SELECT length(vcf) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM addressobjects WHERE principal = ? AND addressbook_id = ?",
|
||||||
|
principal,
|
||||||
|
addressbook_id
|
||||||
|
)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await.map_err(crate::Error::from)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| (row.length, row.deleted))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -442,6 +464,29 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
Self::_sync_changes(&self.db, principal, addressbook_id, synctoken).await
|
Self::_sync_changes(&self.db, principal, addressbook_id, synctoken).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn addressbook_metadata(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
addressbook_id: &str,
|
||||||
|
) -> Result<CollectionMetadata, rustical_store::Error> {
|
||||||
|
let mut sizes = vec![];
|
||||||
|
let mut deleted_sizes = vec![];
|
||||||
|
for (size, deleted) in Self::_list_objects(&self.db, principal, addressbook_id).await? {
|
||||||
|
if deleted {
|
||||||
|
deleted_sizes.push(size)
|
||||||
|
} else {
|
||||||
|
sizes.push(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(CollectionMetadata {
|
||||||
|
len: sizes.len(),
|
||||||
|
deleted_len: deleted_sizes.len(),
|
||||||
|
size: sizes.iter().sum(),
|
||||||
|
deleted_size: deleted_sizes.iter().sum(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn get_objects(
|
async fn get_objects(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use derive_more::derive::Constructor;
|
|||||||
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
|
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
|
||||||
use rustical_store::calendar_store::CalendarQuery;
|
use rustical_store::calendar_store::CalendarQuery;
|
||||||
use rustical_store::synctoken::format_synctoken;
|
use rustical_store::synctoken::format_synctoken;
|
||||||
use rustical_store::{Calendar, CalendarStore, Error};
|
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, Error};
|
||||||
use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
||||||
use sqlx::types::chrono::NaiveDateTime;
|
use sqlx::types::chrono::NaiveDateTime;
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
@@ -242,6 +242,28 @@ impl SqliteCalendarStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
|
executor: E,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
) -> Result<Vec<(u64, bool)>, rustical_store::Error> {
|
||||||
|
struct ObjectEntry {
|
||||||
|
length: u64,
|
||||||
|
deleted: bool,
|
||||||
|
}
|
||||||
|
Ok(sqlx::query_as!(
|
||||||
|
ObjectEntry,
|
||||||
|
"SELECT length(ics) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM calendarobjects WHERE principal = ? AND cal_id = ?",
|
||||||
|
principal,
|
||||||
|
cal_id
|
||||||
|
)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await.map_err(crate::Error::from)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| (row.length, row.deleted))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -552,6 +574,28 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Self::_calendar_query(&self.db, principal, cal_id, query).await
|
Self::_calendar_query(&self.db, principal, cal_id, query).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn calendar_metadata(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
) -> Result<CollectionMetadata, Error> {
|
||||||
|
let mut sizes = vec![];
|
||||||
|
let mut deleted_sizes = vec![];
|
||||||
|
for (size, deleted) in Self::_list_objects(&self.db, principal, cal_id).await? {
|
||||||
|
if deleted {
|
||||||
|
deleted_sizes.push(size)
|
||||||
|
} else {
|
||||||
|
sizes.push(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(CollectionMetadata {
|
||||||
|
len: sizes.len(),
|
||||||
|
deleted_len: deleted_sizes.len(),
|
||||||
|
size: sizes.iter().sum(),
|
||||||
|
deleted_size: deleted_sizes.iter().sum(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn get_objects(
|
async fn get_objects(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ impl Enum {
|
|||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics ::rustical_xml::XmlSerialize for #ident #type_generics #where_clause {
|
impl #impl_generics ::rustical_xml::XmlSerialize for #ident #type_generics #where_clause {
|
||||||
fn serialize<W: ::std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<::quick_xml::name::Namespace>,
|
ns: Option<::quick_xml::name::Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &[u8]>,
|
namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &[u8]>,
|
||||||
writer: &mut ::quick_xml::Writer<W>
|
writer: &mut ::quick_xml::Writer<&mut Vec<u8>>
|
||||||
) -> ::std::io::Result<()> {
|
) -> ::std::io::Result<()> {
|
||||||
use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||||
|
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ impl NamedStruct {
|
|||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics ::rustical_xml::XmlSerialize for #ident #type_generics #where_clause {
|
impl #impl_generics ::rustical_xml::XmlSerialize for #ident #type_generics #where_clause {
|
||||||
fn serialize<W: ::std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<::quick_xml::name::Namespace>,
|
ns: Option<::quick_xml::name::Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &[u8]>,
|
namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &[u8]>,
|
||||||
writer: &mut ::quick_xml::Writer<W>
|
writer: &mut ::quick_xml::Writer<&mut Vec<u8>>
|
||||||
) -> ::std::io::Result<()> {
|
) -> ::std::io::Result<()> {
|
||||||
use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||||
|
|
||||||
|
|||||||
@@ -7,24 +7,24 @@ use std::collections::HashMap;
|
|||||||
pub use xml_derive::XmlSerialize;
|
pub use xml_derive::XmlSerialize;
|
||||||
|
|
||||||
pub trait XmlSerialize {
|
pub trait XmlSerialize {
|
||||||
fn serialize<W: std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()>;
|
) -> std::io::Result<()>;
|
||||||
|
|
||||||
fn attributes<'a>(&self) -> Option<impl IntoIterator<Item: Into<Attribute<'a>>>>;
|
fn attributes<'a>(&self) -> Option<Vec<Attribute<'a>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: XmlSerialize> XmlSerialize for Option<T> {
|
impl<T: XmlSerialize> XmlSerialize for Option<T> {
|
||||||
fn serialize<W: std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
if let Some(some) = self {
|
if let Some(some) = self {
|
||||||
some.serialize(ns, tag, namespaces, writer)
|
some.serialize(ns, tag, namespaces, writer)
|
||||||
@@ -33,17 +33,13 @@ impl<T: XmlSerialize> XmlSerialize for Option<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(refining_impl_trait)]
|
|
||||||
fn attributes<'a>(&self) -> Option<Vec<Attribute<'a>>> {
|
fn attributes<'a>(&self) -> Option<Vec<Attribute<'a>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait XmlSerializeRoot {
|
pub trait XmlSerializeRoot {
|
||||||
fn serialize_root<W: std::io::Write>(
|
fn serialize_root(&self, writer: &mut quick_xml::Writer<&mut Vec<u8>>) -> std::io::Result<()>;
|
||||||
&self,
|
|
||||||
writer: &mut quick_xml::Writer<W>,
|
|
||||||
) -> std::io::Result<()>;
|
|
||||||
|
|
||||||
fn serialize_to_string(&self) -> std::io::Result<String> {
|
fn serialize_to_string(&self) -> std::io::Result<String> {
|
||||||
let mut buf: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
let mut buf: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
||||||
@@ -54,22 +50,19 @@ pub trait XmlSerializeRoot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T: XmlSerialize + XmlRootTag> XmlSerializeRoot for T {
|
impl<T: XmlSerialize + XmlRootTag> XmlSerializeRoot for T {
|
||||||
fn serialize_root<W: std::io::Write>(
|
fn serialize_root(&self, writer: &mut quick_xml::Writer<&mut Vec<u8>>) -> std::io::Result<()> {
|
||||||
&self,
|
|
||||||
writer: &mut quick_xml::Writer<W>,
|
|
||||||
) -> std::io::Result<()> {
|
|
||||||
let namespaces = Self::root_ns_prefixes();
|
let namespaces = Self::root_ns_prefixes();
|
||||||
self.serialize(Self::root_ns(), Some(Self::root_tag()), &namespaces, writer)
|
self.serialize(Self::root_ns(), Some(Self::root_tag()), &namespaces, writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XmlSerialize for () {
|
impl XmlSerialize for () {
|
||||||
fn serialize<W: std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
let prefix = ns
|
let prefix = ns
|
||||||
.map(|ns| namespaces.get(&ns))
|
.map(|ns| namespaces.get(&ns))
|
||||||
@@ -96,7 +89,6 @@ impl XmlSerialize for () {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(refining_impl_trait)]
|
|
||||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,12 +104,12 @@ impl<T: ValueDeserialize> XmlDeserialize for T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ValueSerialize> XmlSerialize for T {
|
impl<T: ValueSerialize> XmlSerialize for T {
|
||||||
fn serialize<W: std::io::Write>(
|
fn serialize(
|
||||||
&self,
|
&self,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut quick_xml::Writer<W>,
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
let prefix = ns
|
let prefix = ns
|
||||||
.map(|ns| namespaces.get(&ns))
|
.map(|ns| namespaces.get(&ns))
|
||||||
@@ -140,7 +140,6 @@ impl<T: ValueSerialize> XmlSerialize for T {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(refining_impl_trait)]
|
|
||||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,12 +124,12 @@ fn test_struct_serialize_with() {
|
|||||||
href: String,
|
href: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_href<W: ::std::io::Write>(
|
fn serialize_href(
|
||||||
val: &str,
|
val: &str,
|
||||||
ns: Option<Namespace>,
|
ns: Option<Namespace>,
|
||||||
tag: Option<&[u8]>,
|
tag: Option<&[u8]>,
|
||||||
namespaces: &HashMap<Namespace, &[u8]>,
|
namespaces: &HashMap<Namespace, &[u8]>,
|
||||||
writer: &mut Writer<W>,
|
writer: &mut Writer<&mut Vec<u8>>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
val.to_uppercase().serialize(ns, tag, namespaces, writer)
|
val.to_uppercase().serialize(ns, tag, namespaces, writer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
use argon2::password_hash::SaltString;
|
|
||||||
use clap::{Parser, ValueEnum};
|
|
||||||
use password_hash::{PasswordHasher, rand_core::OsRng};
|
|
||||||
use pbkdf2::Params;
|
|
||||||
use rustical_frontend::FrontendConfig;
|
|
||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
Config, DataStoreConfig, DavPushConfig, HttpConfig, SqliteDataStoreConfig, TracingConfig,
|
Config, DataStoreConfig, DavPushConfig, HttpConfig, SqliteDataStoreConfig, TracingConfig,
|
||||||
};
|
};
|
||||||
|
use clap::Parser;
|
||||||
|
use rustical_frontend::FrontendConfig;
|
||||||
|
|
||||||
mod membership;
|
mod membership;
|
||||||
pub mod principals;
|
pub mod principals;
|
||||||
@@ -33,49 +29,3 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
|
|||||||
println!("{generated_config}");
|
println!("{generated_config}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, ValueEnum)]
|
|
||||||
enum PwhashAlgorithm {
|
|
||||||
#[value(help = "Use this for your password")]
|
|
||||||
Argon2,
|
|
||||||
#[value(help = "Significantly faster algorithm, use for app tokens")]
|
|
||||||
Pbkdf2,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
pub struct PwhashArgs {
|
|
||||||
#[arg(long, short = 'a')]
|
|
||||||
algorithm: PwhashAlgorithm,
|
|
||||||
#[arg(
|
|
||||||
long,
|
|
||||||
short = 'r',
|
|
||||||
help = "ONLY for pbkdf2: Number of rounds to calculate",
|
|
||||||
default_value_t = 100
|
|
||||||
)]
|
|
||||||
rounds: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cmd_pwhash(args: PwhashArgs) -> anyhow::Result<()> {
|
|
||||||
println!("Enter your password:");
|
|
||||||
let password = rpassword::read_password()?;
|
|
||||||
let salt = SaltString::generate(OsRng);
|
|
||||||
let password_hash = match args.algorithm {
|
|
||||||
PwhashAlgorithm::Argon2 => argon2::Argon2::default()
|
|
||||||
.hash_password(password.as_bytes(), &salt)
|
|
||||||
.unwrap(),
|
|
||||||
PwhashAlgorithm::Pbkdf2 => pbkdf2::Pbkdf2
|
|
||||||
.hash_password_customized(
|
|
||||||
password.as_bytes(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Params {
|
|
||||||
rounds: args.rounds,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&salt,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
};
|
|
||||||
println!("{password_hash}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use app::make_app;
|
|||||||
use axum::ServiceExt;
|
use axum::ServiceExt;
|
||||||
use axum::extract::Request;
|
use axum::extract::Request;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
use commands::cmd_gen_config;
|
||||||
use commands::principals::{PrincipalsArgs, cmd_principals};
|
use commands::principals::{PrincipalsArgs, cmd_principals};
|
||||||
use commands::{cmd_gen_config, cmd_pwhash};
|
|
||||||
use config::{DataStoreConfig, SqliteDataStoreConfig};
|
use config::{DataStoreConfig, SqliteDataStoreConfig};
|
||||||
use figment::Figment;
|
use figment::Figment;
|
||||||
use figment::providers::{Env, Format, Toml};
|
use figment::providers::{Env, Format, Toml};
|
||||||
@@ -43,7 +43,6 @@ struct Args {
|
|||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
enum Command {
|
enum Command {
|
||||||
GenConfig(commands::GenConfigArgs),
|
GenConfig(commands::GenConfigArgs),
|
||||||
Pwhash(commands::PwhashArgs),
|
|
||||||
Principals(PrincipalsArgs),
|
Principals(PrincipalsArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +83,6 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Some(Command::GenConfig(gen_config_args)) => cmd_gen_config(gen_config_args)?,
|
Some(Command::GenConfig(gen_config_args)) => cmd_gen_config(gen_config_args)?,
|
||||||
Some(Command::Pwhash(pwhash_args)) => cmd_pwhash(pwhash_args)?,
|
|
||||||
Some(Command::Principals(principals_args)) => cmd_principals(principals_args).await?,
|
Some(Command::Principals(principals_args)) => cmd_principals(principals_args).await?,
|
||||||
None => {
|
None => {
|
||||||
let config: Config = Figment::new()
|
let config: Config = Figment::new()
|
||||||
|
|||||||
Reference in New Issue
Block a user