mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
frontend: Add button to restore deleted collections
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
43
crates/frontend/src/routes/addressbook.rs
Normal file
43
crates/frontend/src/routes/addressbook.rs
Normal 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"),
|
||||||
|
})
|
||||||
|
}
|
||||||
43
crates/frontend/src/routes/calendar.rs
Normal file
43
crates/frontend/src/routes/calendar.rs
Normal 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"),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1 +1,4 @@
|
|||||||
|
pub mod addressbook;
|
||||||
|
pub mod calendar;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user