frontend: Add button to restore deleted collections

This commit is contained in:
Lennart
2024-11-10 14:42:11 +01:00
parent 2d65ebb57b
commit 29313b27e9
6 changed files with 119 additions and 43 deletions

View File

@@ -23,11 +23,14 @@ docker run -p 4000:4000 -v YOUR_CONFIG_TOML:/etc/rustical/config.toml -v YOUR_DA
## Configuration ## Configuration
You can generate a default `config.toml` with You can generate a default `config.toml` with
```sh ```sh
rustical gen-config rustical gen-config
``` ```
There, you can customize your username, password, and app tokens. There, you can customize your username, password, and app tokens.
Password hashes can be generated with Password hashes can be generated with
```sh ```sh
rustical pwhash rustical pwhash
``` ```
@@ -40,7 +43,6 @@ Since it's sensitive information, the secure but slow hash algorithm `argon2` is
I recommend to generate random app tokens for each CalDAV/CardDAV client. I recommend to generate random app tokens for each CalDAV/CardDAV client.
These can use the faster `pbkdf2` algorithm. These can use the faster `pbkdf2` algorithm.
## Todo ## Todo
- [ ] CalDAV - [ ] CalDAV
@@ -64,7 +66,7 @@ These can use the faster `pbkdf2` algorithm.
- [ ] Web UI - [ ] Web UI
- [x] Trash bin - [x] Trash bin
- [x] Hiding calendars instead of deleting them - [x] Hiding calendars instead of deleting them
- [ ] Restore endpoint - [x] Restore endpoint
- [ ] Packaging - [ ] Packaging
- [x] Ensure cargo install works - [x] Ensure cargo install works
- [x] Docker image - [x] Docker image

View File

@@ -16,8 +16,9 @@ li.collection-list-item {
". color-chip" ". color-chip"
"title color-chip" "title color-chip"
"description color-chip" "description color-chip"
"restore color-chip"
". color-chip"; ". color-chip";
grid-template-rows: 12px auto auto 12px; grid-template-rows: 12px auto auto auto 12px;
grid-template-columns: auto 50px; grid-template-columns: auto 50px;
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
@@ -37,6 +38,10 @@ li.collection-list-item {
border-radius: 0 12px 12px 0; border-radius: 0 12px 12px 0;
} }
.restore-form {
grid-area: restore;
}
&:hover { &:hover {
background: #DDD; background: #DDD;
} }
@@ -71,6 +76,9 @@ li.collection-list-item {
<span class="description"> <span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %} {% if let Some(description) = calendar.description %}{{ description }}{% endif %}
</span> </span>
<form action="/frontend/user/{{ user_id }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button>
</form>
<div class="color-chip"></div> <div class="color-chip"></div>
</a> </a>
</li> </li>
@@ -101,6 +109,9 @@ li.collection-list-item {
<span class="description"> <span class="description">
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %} {% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
</span> </span>
<form action="/frontend/user/{{ user_id }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button>
</form>
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@@ -12,7 +12,11 @@ use actix_web::{
use askama::Template; use askama::Template;
use askama_actix::TemplateToResponse; use askama_actix::TemplateToResponse;
use assets::{Assets, EmbedService}; use assets::{Assets, EmbedService};
use routes::login::{route_get_login, route_post_login}; use routes::{
addressbook::{route_addressbook, route_addressbook_restore},
calendar::{route_calendar, route_calendar_restore},
login::{route_get_login, route_post_login},
};
use rustical_store::{ use rustical_store::{
auth::{AuthenticationMiddleware, AuthenticationProvider, User}, auth::{AuthenticationMiddleware, AuthenticationProvider, User},
Addressbook, AddressbookStore, Calendar, CalendarStore, Addressbook, AddressbookStore, Calendar, CalendarStore,
@@ -57,44 +61,6 @@ async fn route_user<CS: CalendarStore + ?Sized, AS: AddressbookStore + ?Sized>(
.to_response() .to_response()
} }
#[derive(Template)]
#[template(path = "pages/calendar.html")]
struct CalendarPage {
owner: String,
calendar: Calendar,
}
async fn route_calendar<C: CalendarStore + ?Sized>(
path: Path<(String, String)>,
store: Data<C>,
_user: User,
) -> Result<impl Responder, rustical_store::Error> {
let (owner, cal_id) = path.into_inner();
Ok(CalendarPage {
owner: owner.to_owned(),
calendar: store.get_calendar(&owner, &cal_id).await?,
})
}
#[derive(Template)]
#[template(path = "pages/addressbook.html")]
struct AddressbookPage {
owner: String,
addressbook: Addressbook,
}
async fn route_addressbook<AS: AddressbookStore + ?Sized>(
path: Path<(String, String)>,
store: Data<AS>,
_user: User,
) -> Result<impl Responder, rustical_store::Error> {
let (owner, addrbook_id) = path.into_inner();
Ok(AddressbookPage {
owner: owner.to_owned(),
addressbook: store.get_addressbook(&owner, &addrbook_id).await?,
})
}
async fn route_root(user: Option<User>, req: HttpRequest) -> impl Responder { async fn route_root(user: Option<User>, req: HttpRequest) -> impl Responder {
let redirect_url = match user { let redirect_url = match user {
Some(user) => req Some(user) => req
@@ -173,9 +139,17 @@ pub fn configure_frontend<
.route(web::method(Method::GET).to(route_calendar::<CS>)), .route(web::method(Method::GET).to(route_calendar::<CS>)),
) )
.service( .service(
web::resource("/user/{user}/addressbook/{calendar}") web::resource("/user/{user}/calendar/{calendar}/restore")
.route(web::method(Method::POST).to(route_calendar_restore::<CS>)),
)
.service(
web::resource("/user/{user}/addressbook/{addressbook}")
.route(web::method(Method::GET).to(route_addressbook::<AS>)), .route(web::method(Method::GET).to(route_addressbook::<AS>)),
) )
.service(
web::resource("/user/{user}/addressbook/{addressbook}/restore")
.route(web::method(Method::POST).to(route_addressbook_restore::<AS>)),
)
.service( .service(
web::resource("/login") web::resource("/login")
.name("frontend_login") .name("frontend_login")

View File

@@ -0,0 +1,43 @@
use actix_web::{
http::{header, StatusCode},
web::{self, Data, Path},
HttpRequest, HttpResponse, Responder,
};
use askama::Template;
use rustical_store::{auth::User, Addressbook, AddressbookStore};
#[derive(Template)]
#[template(path = "pages/addressbook.html")]
struct AddressbookPage {
owner: String,
addressbook: Addressbook,
}
pub async fn route_addressbook<AS: AddressbookStore + ?Sized>(
path: Path<(String, String)>,
store: Data<AS>,
_user: User,
) -> Result<impl Responder, rustical_store::Error> {
let (owner, addrbook_id) = path.into_inner();
Ok(AddressbookPage {
owner: owner.to_owned(),
addressbook: store.get_addressbook(&owner, &addrbook_id).await?,
})
}
pub async fn route_addressbook_restore<AS: AddressbookStore + ?Sized>(
path: Path<(String, String)>,
req: HttpRequest,
store: Data<AS>,
_user: User,
) -> Result<impl Responder, rustical_store::Error> {
let (owner, addressbook_id) = path.into_inner();
store.restore_addressbook(&owner, &addressbook_id).await?;
Ok(match req.headers().get(header::REFERER) {
Some(referer) => web::Redirect::to(referer.to_str().unwrap().to_owned())
.using_status_code(StatusCode::FOUND)
.respond_to(&req)
.map_into_boxed_body(),
None => HttpResponse::Ok().body("Restored"),
})
}

View File

@@ -0,0 +1,43 @@
use actix_web::{
http::{header, StatusCode},
web::{self, Data, Path},
HttpRequest, HttpResponse, Responder,
};
use askama::Template;
use rustical_store::{auth::User, Calendar, CalendarStore};
#[derive(Template)]
#[template(path = "pages/calendar.html")]
struct CalendarPage {
owner: String,
calendar: Calendar,
}
pub async fn route_calendar<C: CalendarStore + ?Sized>(
path: Path<(String, String)>,
store: Data<C>,
_user: User,
) -> Result<impl Responder, rustical_store::Error> {
let (owner, cal_id) = path.into_inner();
Ok(CalendarPage {
owner: owner.to_owned(),
calendar: store.get_calendar(&owner, &cal_id).await?,
})
}
pub async fn route_calendar_restore<CS: CalendarStore + ?Sized>(
path: Path<(String, String)>,
req: HttpRequest,
store: Data<CS>,
_user: User,
) -> Result<impl Responder, rustical_store::Error> {
let (owner, cal_id) = path.into_inner();
store.restore_calendar(&owner, &cal_id).await?;
Ok(match req.headers().get(header::REFERER) {
Some(referer) => web::Redirect::to(referer.to_str().unwrap().to_owned())
.using_status_code(StatusCode::FOUND)
.respond_to(&req)
.map_into_boxed_body(),
None => HttpResponse::Ok().body("Restored"),
})
}

View File

@@ -1 +1,4 @@
pub mod addressbook;
pub mod calendar;
pub mod login; pub mod login;