refactor: use rinja instead of askama

This commit is contained in:
Max Hohlfeld 2024-12-16 13:50:12 +01:00
parent 187f13a938
commit 4d04069f48
49 changed files with 405 additions and 345 deletions

112
Cargo.lock generated
View File

@ -379,60 +379,6 @@ dependencies = [
"password-hash",
]
[[package]]
name = "askama"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
dependencies = [
"askama_derive",
"askama_escape",
"humansize",
"num-traits",
"percent-encoding",
]
[[package]]
name = "askama_actix"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4b0dd17cfe203b00ba3853a89fba459ecf24c759b738b244133330607c78e55"
dependencies = [
"actix-web",
"askama",
]
[[package]]
name = "askama_derive"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
dependencies = [
"askama_parser",
"basic-toml",
"mime",
"mime_guess",
"proc-macro2",
"quote",
"serde",
"syn",
]
[[package]]
name = "askama_escape"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "askama_parser"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
dependencies = [
"nom",
]
[[package]]
name = "async-channel"
version = "1.9.0"
@ -738,8 +684,6 @@ dependencies = [
"actix-web-static-files",
"anyhow",
"argon2",
"askama",
"askama_actix",
"async-trait",
"brass-config",
"brass-macros",
@ -754,6 +698,7 @@ dependencies = [
"quick-xml",
"rand",
"regex",
"rinja",
"serde",
"serde_json",
"sqlx",
@ -818,9 +763,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.3"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d"
checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
dependencies = [
"jobserver",
"libc",
@ -2568,6 +2513,47 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rinja"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dc4940d00595430b3d7d5a01f6222b5e5b51395d1120bdb28d854bb8abb17a5"
dependencies = [
"humansize",
"itoa",
"percent-encoding",
"rinja_derive",
]
[[package]]
name = "rinja_derive"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d9ed0146aef6e2825f1b1515f074510549efba38d71f4554eec32eb36ba18b"
dependencies = [
"basic-toml",
"memchr",
"mime",
"mime_guess",
"proc-macro2",
"quote",
"rinja_parser",
"rustc-hash",
"serde",
"syn",
]
[[package]]
name = "rinja_parser"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610"
dependencies = [
"memchr",
"nom",
"serde",
]
[[package]]
name = "rsa"
version = "0.9.7"
@ -2594,6 +2580,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "rustc_version"
version = "0.4.1"
@ -2716,9 +2708,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.23"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
[[package]]
name = "serde"

View File

@ -10,7 +10,8 @@ publish = false
[dependencies]
sqlx = { version = "^0.8", features = ["runtime-async-std-rustls", "postgres", "chrono"] }
actix-web = { version = "4" }
askama = { version = "0.12.0", features = ["with-actix-web"] }
# askama = { version = "0.13.0", git = "https://github.com/rinja-rs/askama", branch = "main", features = ["with-actix-web"] }
# askama_actix = { git = "https://github.com/rinja-rs/askama", branch = "main" }
serde = { version = "1.0.164", features = ["derive"] }
argon2 = { version = "0.5.0", features = [ "std"]}
anyhow = "1.0.71"
@ -19,7 +20,6 @@ actix-session = { version = "0.10.1", features = ["cookie-session"] }
actix-identity = "0.8.0"
chrono = { version = "0.4.33", features = ["serde", "now"] }
actix-files = "0.6.5"
askama_actix = "0.14.0"
futures-util = "0.3.30"
serde_json = "1.0.114"
pico-args = "0.5.0"
@ -36,6 +36,7 @@ regex = "1.11.1"
brass-macros = { path = "../macros" }
brass-config = { path = "../config" }
actix-http = "3.9.0"
rinja = "0.3.5"
[build-dependencies]
built = "0.7.4"
@ -44,8 +45,5 @@ static-files = "0.2.1"
[dev-dependencies]
insta = "1.41.1"
# [dev-dependencies]
# brass-web = { path = "." }
# [profile.dev.package.askama_derive]
# opt-level = 3

View File

@ -0,0 +1,56 @@
---
source: web/src/endpoints/area/get_new.rs
expression: body
snapshot_kind: text
---
<section class="section">
<div class="container">
<form method="post" action="/area/new">
<h1 class="title">Neuen Bereich anlegen</h1>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Name</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" type="text" name="name" required placeholder="Leipzig Ost" />
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label"></div>
<div class="field-body">
<div class="field is-grouped">
<div class="control">
<button class="button is-success">
<svg class="icon">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
<span>
Erstellen
</span>
</button>
</div>
<div class="control">
<a class="button is-link is-light" hx-boost="true" href="/locations">
<svg class="icon">
<use href="/static/feather-sprite.svg#arrow-left" />
</svg>
<span>Zurück</span>
</a>
</div>
</div>
</div>
</div>
</form>
</div>
</section>

View File

@ -1,5 +1,5 @@
use actix_web::{web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use rinja::Template;
use sqlx::PgPool;
use crate::{
@ -24,7 +24,7 @@ async fn get(
area: Some(area_in_db),
};
return Ok(template.to_response());
return Ok(HttpResponse::Ok().body(template.render()?))
} else {
return Ok(HttpResponse::NotFound().finish());
}

View File

@ -1,23 +1,23 @@
use actix_web::{body::MessageBody, test::read_body, web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use brass_macros::db_test;
use insta::assert_snapshot;
use crate::{
endpoints::area::NewOrEditAreaTemplate,
models::{Role, User},
utils::test_helper::DbTestContext,
utils::ApplicationError,
};
use actix_web::{web, HttpResponse, Responder};
use brass_macros::db_test;
use rinja::Template;
#[cfg(test)]
use crate::utils::test_helper::{test_get, RequestConfig};
use crate::utils::test_helper::{
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig,
};
#[cfg(test)]
use actix_http::StatusCode;
#[actix_web::get("/area/new")]
async fn get(user: web::ReqData<User>) -> impl Responder {
async fn get(user: web::ReqData<User>) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin {
return HttpResponse::Unauthorized().finish();
return Err(ApplicationError::Unauthorized);
}
let template = NewOrEditAreaTemplate {
@ -25,7 +25,7 @@ async fn get(user: web::ReqData<User>) -> impl Responder {
area: None,
};
template.to_response()
Ok(HttpResponse::Ok().body(template.render()?))
}
#[db_test]
@ -41,6 +41,6 @@ async fn produces_template(context: &DbTestContext) {
assert_eq!(StatusCode::OK, response.status());
let body = String::from_utf8(read_body(response).await.to_vec()).unwrap();
let body = read_body(response).await;
assert_snapshot!(body);
}

View File

@ -4,13 +4,14 @@ pub mod get_edit;
pub mod post_edit;
pub mod delete;
use askama::Template;
use rinja::Template;
use serde::Deserialize;
use crate::models::{Area, Role, User};
#[derive(Template)]
#[template(path = "area/new_or_edit.html")]
#[cfg_attr(not(test), template(path = "area/new_or_edit.html"))]
#[cfg_attr(test, template(path = "area/new_or_edit.html", block = "content"))]
struct NewOrEditAreaTemplate {
user: User,
area: Option<Area>,

View File

@ -1,5 +1,5 @@
use actix_web::{web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool;
@ -61,5 +61,5 @@ pub async fn delete(
further_wachhabender_required,
};
Ok(template.to_response())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,4 +1,4 @@
use askama::Template;
use rinja::Template;
use crate::{
filters,

View File

@ -1,7 +1,5 @@
use std::ops::AddAssign;
use actix_web::{web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool;
@ -108,5 +106,5 @@ pub async fn post(
further_wachhabender_required,
};
Ok(template.to_response())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,10 +1,11 @@
use actix_web::{web, Responder};
use askama_actix::TemplateToResponse;
use actix_web::{web, HttpResponse, Responder};
use chrono::NaiveDate;
use rinja::Template;
use serde::Deserialize;
use crate::endpoints::availability::NewOrEditAvailabilityTemplate;
use crate::models::User;
use crate::utils::ApplicationError;
#[derive(Deserialize)]
struct AvailabilityNewQuery {
@ -17,7 +18,7 @@ struct AvailabilityNewQuery {
pub async fn get(
user: web::ReqData<User>,
query: web::Query<AvailabilityNewQuery>,
) -> impl Responder {
) -> Result<impl Responder, ApplicationError> {
let template = NewOrEditAvailabilityTemplate {
user: user.into_inner(),
date: query.date,
@ -28,5 +29,5 @@ pub async fn get(
comment: None,
};
template.to_response()
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,8 +1,7 @@
use crate::filters;
use crate::{filters, utils::ApplicationError};
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use chrono::{NaiveDate, Utc};
use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool;
@ -30,18 +29,18 @@ async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
query: web::Query<CalendarQuery>,
) -> impl Responder {
) -> Result<impl Responder, ApplicationError> {
let date = match query.date {
Some(given_date) => given_date,
None => Utc::now().date_naive(),
};
let areas = Area::read_all(pool.get_ref()).await.unwrap();
let areas = Area::read_all(pool.get_ref()).await?;
let selected_area = match query.area {
Some(id) => {
if !areas.iter().any(|a| a.id == id) {
return HttpResponse::BadRequest().finish();
return Ok(HttpResponse::BadRequest().finish());
}
Some(id)
@ -54,16 +53,14 @@ async fn get(
date,
query.area.unwrap_or(user.area_id),
)
.await
.unwrap();
.await?;
let availabillities = Availabillity::read_by_date_and_area_including_user(
pool.get_ref(),
date,
query.area.unwrap_or(user.area_id),
)
.await
.unwrap();
.await?;
let template = CalendarTemplate {
user: user.into_inner(),
@ -74,5 +71,5 @@ async fn get(
availabillities,
};
template.to_response()
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,5 +1,5 @@
use actix_web::{web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool;
@ -50,5 +50,5 @@ pub async fn get(
comment: availabillity.comment.as_deref(),
};
Ok(template.to_response())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,4 +1,4 @@
use askama::Template;
use rinja::Template;
use chrono::NaiveDate;
use crate::filters;

View File

@ -1,7 +1,6 @@
use actix_web::{web, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use actix_web::{web, HttpResponse, Responder};
use chrono::NaiveDate;
use rinja::Template;
use sqlx::PgPool;
use crate::{
@ -40,5 +39,5 @@ pub async fn get(
locations,
};
Ok(template.to_response())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,6 +1,5 @@
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use rinja::Template;
use sqlx::PgPool;
use crate::{
@ -61,5 +60,5 @@ pub async fn get(
further_wachhabender_required,
};
Ok(template.to_response())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,9 +1,8 @@
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use rinja::Template;
use sqlx::PgPool;
use crate::models::{Area, User, Role};
use crate::{models::{Area, Role, User}, utils::ApplicationError};
#[derive(Template)]
#[template(path = "export/availability.html")]
@ -16,13 +15,13 @@ struct AvailabilityExportTemplate {
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>
) -> impl Responder {
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin && user.role != Role::AreaManager {
return HttpResponse::Unauthorized().finish();
return Err(ApplicationError::Unauthorized);
}
let areas = if user.role == Role::Admin {
Some(Area::read_all(pool.get_ref()).await.unwrap())
Some(Area::read_all(pool.get_ref()).await?)
} else {
None
};
@ -32,5 +31,5 @@ pub async fn get(
areas
};
return template.to_response();
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -40,9 +40,10 @@ pub async fn get(
user: Identity,
query: web::Query<ExportQuery>,
) -> impl Responder {
// TODO: rerwrite
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
.unwrap().unwrap();
if current_user.role != Role::Admin && current_user.role != Role::AreaManager {
return HttpResponse::Unauthorized().finish();

View File

@ -1,14 +1,15 @@
use actix_web::Responder;
use askama::Template;
use askama_actix::TemplateToResponse;
use actix_web::{HttpResponse, Responder};
use rinja::Template;
use crate::utils::ApplicationError;
#[derive(Template)]
#[template(path = "imprint.html")]
struct ImprintTemplate {}
#[actix_web::get("/imprint")]
pub async fn get_imprint() -> impl Responder {
pub async fn get_imprint() -> Result<impl Responder, ApplicationError> {
let template = ImprintTemplate {};
template.to_response()
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,5 +1,5 @@
use actix_web::{web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use rinja::Template;
use sqlx::PgPool;
use crate::{
@ -38,5 +38,5 @@ pub async fn get(
location: Some(location),
};
Ok(template.to_response())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,26 +1,33 @@
use actix_web::{web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use rinja::Template;
use sqlx::PgPool;
use crate::{endpoints::location::LocationTemplate, models::{Area, Role, User}};
use crate::{
endpoints::location::LocationTemplate,
models::{Area, Role, User},
utils::ApplicationError,
};
#[actix_web::get("/locations/new")]
pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl Responder {
if user.role == Role::AreaManager || user.role == Role::Admin {
let mut areas = None;
if user.role == Role::Admin {
areas = Some(Area::read_all(pool.get_ref()).await.unwrap());
}
let template = LocationTemplate {
user: user.into_inner(),
areas,
location: None
};
return template.to_response();
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::AreaManager && user.role != Role::Admin {
return Err(ApplicationError::Unauthorized);
}
return HttpResponse::Unauthorized().finish();
let mut areas = None;
if user.role == Role::Admin {
areas = Some(Area::read_all(pool.get_ref()).await?);
}
let template = LocationTemplate {
user: user.into_inner(),
areas,
location: None,
};
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,9 +1,11 @@
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use rinja::Template;
use sqlx::PgPool;
use crate::models::{Area, Location, Role, User};
use crate::{
models::{Area, Location, Role, User},
utils::ApplicationError,
};
#[derive(Template)]
#[template(path = "location/overview.html")]
@ -13,45 +15,45 @@ pub struct LocationsTemplate {
}
#[actix_web::get("/locations")]
pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl Responder {
if user.role == Role::AreaManager || user.role == Role::Admin {
let mut locations;
let mut grouped_locations: Vec<(Area, Vec<Location>)>;
if user.role == Role::Admin {
locations = Location::read_all(pool.get_ref()).await.unwrap();
grouped_locations = Area::read_all(pool.get_ref())
.await
.unwrap()
.into_iter()
.map(|a| (a, Vec::new()))
.collect();
} else {
locations = Location::read_by_area(pool.get_ref(), user.area_id)
.await
.unwrap();
let area = Area::read_by_id(pool.get_ref(), user.area_id)
.await
.unwrap().unwrap();
grouped_locations = vec![(area, Vec::new())];
}
for entry in grouped_locations.iter_mut() {
let (mut locations_in_this_area, rest): (Vec<_>, Vec<_>) =
locations.into_iter().partition(|l| l.area_id == entry.0.id);
locations = rest;
entry.1.append(&mut locations_in_this_area);
}
let template = LocationsTemplate {
user: user.into_inner(),
grouped_locations,
};
return template.to_response();
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::AreaManager && user.role != Role::Admin {
return Err(ApplicationError::Unauthorized);
}
return HttpResponse::Unauthorized().finish();
let mut locations;
let mut grouped_locations: Vec<(Area, Vec<Location>)>;
if user.role == Role::Admin {
locations = Location::read_all(pool.get_ref()).await.unwrap();
grouped_locations = Area::read_all(pool.get_ref())
.await?
.into_iter()
.map(|a| (a, Vec::new()))
.collect();
} else {
locations = Location::read_by_area(pool.get_ref(), user.area_id).await?;
let area = Area::read_by_id(pool.get_ref(), user.area_id)
.await?
.unwrap();
grouped_locations = vec![(area, Vec::new())];
}
for entry in grouped_locations.iter_mut() {
let (mut locations_in_this_area, rest): (Vec<_>, Vec<_>) =
locations.into_iter().partition(|l| l.area_id == entry.0.id);
locations = rest;
entry.1.append(&mut locations_in_this_area);
}
let template = LocationsTemplate {
user: user.into_inner(),
grouped_locations,
};
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,4 +1,4 @@
use askama::Template;
use rinja::Template;
use serde::Deserialize;
use crate::models::{Area, Location, Role, User};

View File

@ -16,7 +16,9 @@ pub async fn delete(
return Err(ApplicationError::Unauthorized);
}
let user_in_db = User::read_by_id(pool.get_ref(), path.id).await?;
let Some(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
if user.role == Role::AreaManager && user.area_id != user_in_db.area_id {
return Err(ApplicationError::Unauthorized);

View File

@ -1,16 +1,15 @@
use actix_web::{web, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use actix_web::{web, HttpResponse, Responder};
use rinja::Template;
use crate::models::User;
use crate::{models::User, utils::ApplicationError};
#[derive(Template)]
#[template(path = "user/profile_change_password.html")]
struct ProfileChangePasswordTemplate {}
#[actix_web::get("/users/changepassword")]
pub async fn get(_user: web::ReqData<User>) -> impl Responder {
pub async fn get(_user: web::ReqData<User>) -> Result<impl Responder, ApplicationError> {
let template = ProfileChangePasswordTemplate {};
template.to_response()
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,10 +1,11 @@
use actix_web::{web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use rinja::Template;
use sqlx::PgPool;
use crate::{
endpoints::{user::NewOrEditUserTemplate, IdPath},
models::{Area, Role, User},
utils::ApplicationError,
};
#[actix_web::get("/users/edit/{id}")]
@ -12,19 +13,22 @@ pub async fn get_edit(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
) -> impl Responder {
) -> Result<impl Responder, ApplicationError> {
let mut areas = None;
if user.role != Role::AreaManager && user.role != Role::Admin {
return HttpResponse::Unauthorized().finish();
return Err(ApplicationError::Unauthorized);
}
if user.role == Role::Admin {
areas = Some(Area::read_all(pool.get_ref()).await.unwrap());
areas = Some(Area::read_all(pool.get_ref()).await?);
}
if let Ok(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await {
let template = NewOrEditUserTemplate {
let Some(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
let template = NewOrEditUserTemplate {
user: user.into_inner(),
id: Some(user_in_db.id),
areas,
@ -35,8 +39,5 @@ pub async fn get_edit(
area_id: Some(user_in_db.area_id),
};
return template.to_response();
}
HttpResponse::BadRequest().body("Fehler beim Bearbeiten des Nutzers")
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,21 +1,22 @@
use actix_identity::Identity;
use actix_web::{http::header::LOCATION, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use rinja::Template;
use crate::utils::ApplicationError;
#[derive(Template)]
#[template(path = "user/login.html")]
struct LoginTemplate {}
#[actix_web::get("/login")]
async fn get(user: Option<Identity>) -> impl Responder {
async fn get(user: Option<Identity>) -> Result<impl Responder, ApplicationError> {
if let Some(_) = user {
return HttpResponse::Found()
Ok(HttpResponse::Found()
.insert_header((LOCATION, "/"))
.finish();
.finish())
} else {
let template = LoginTemplate {};
return template.to_response();
Ok(HttpResponse::Ok().body(template.render()?))
}
}

View File

@ -1,18 +1,22 @@
use actix_web::{web, Responder};
use askama_actix::TemplateToResponse;
use actix_web::{web, HttpResponse, Responder};
use rinja::Template;
use sqlx::PgPool;
use crate::{
endpoints::user::NewOrEditUserTemplate,
models::{Area, Role, User},
utils::ApplicationError,
};
#[actix_web::get("/users/new")]
pub async fn get_new(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl Responder {
pub async fn get_new(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
) -> Result<impl Responder, ApplicationError> {
let mut areas: Option<Vec<Area>> = None;
if user.role == Role::Admin {
areas = Some(Area::read_all(pool.get_ref()).await.unwrap())
areas = Some(Area::read_all(pool.get_ref()).await?)
}
let template = NewOrEditUserTemplate {
@ -26,5 +30,5 @@ pub async fn get_new(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl
area_id: None,
};
template.to_response()
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,8 +1,10 @@
use crate::models::{Area, Role, User, Function};
use actix_identity::Identity;
use crate::{
models::{Area, Function, Role, User},
utils::ApplicationError,
};
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use rinja::Template;
use sqlx::PgPool;
#[derive(Template)]
@ -14,36 +16,33 @@ pub struct UsersTemplate {
}
#[actix_web::get("/users")]
pub async fn get_overview(user: Identity, pool: web::Data<PgPool>) -> impl Responder {
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
if current_user.role == Role::AreaManager || current_user.role == Role::Admin {
let mut area = None;
let users;
if current_user.role == Role::AreaManager {
area = Some(
Area::read_by_id(pool.get_ref(), current_user.area_id)
.await
.unwrap().unwrap(),
);
users = User::read_all_by_area(pool.get_ref(), current_user.area_id)
.await
.unwrap();
} else {
users = User::read_all_including_area(pool.get_ref()).await.unwrap();
}
let template = UsersTemplate {
user: current_user,
area,
users,
};
return template.to_response();
pub async fn get_overview(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::AreaManager && user.role != Role::Admin {
return Err(ApplicationError::Unauthorized);
}
return HttpResponse::BadRequest().body("Fehler beim Zugriff auf die Nutzerverwaltung!");
let mut area = None;
let users;
if user.role == Role::AreaManager {
area = Some(
Area::read_by_id(pool.get_ref(), user.area_id)
.await?
.unwrap(),
);
users = User::read_all_by_area(pool.get_ref(), user.area_id).await?;
} else {
users = User::read_all_including_area(pool.get_ref()).await?;
}
let template = UsersTemplate {
user: user.into_inner(),
area,
users,
};
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,11 +1,11 @@
use actix_web::{web, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use actix_web::{web, HttpResponse, Responder};
use rinja::Template;
use sqlx::PgPool;
use crate::{
filters,
models::{Area, Role, User},
utils::ApplicationError,
};
#[derive(Template)]
@ -15,15 +15,16 @@ struct ProfileTemplate {
}
#[actix_web::get("/profile")]
pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl Responder {
let area = Area::read_by_id(pool.get_ref(), user.area_id)
.await
.unwrap();
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
) -> Result<impl Responder, ApplicationError> {
let area = Area::read_by_id(pool.get_ref(), user.area_id).await?;
let mut user = user.into_inner();
user.area = area;
let template = ProfileTemplate { user };
return template.to_response();
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,6 +1,6 @@
use actix_identity::Identity;
use actix_web::{get, http::header::LOCATION, web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool;
@ -25,18 +25,18 @@ pub async fn get(
.finish());
}
if let Some(_) = Registration::does_token_exist(pool.get_ref(), &query.token).await? {
let template = ResetPasswordTemplate {
token: &query.token,
title: "Brass - Registrierung",
endpoint: "/register",
new_password_label: "Passwort:",
retype_label: "Passwort wiederholen:",
submit_button_label: "Registrieren",
};
let Some(_) = Registration::does_token_exist(pool.get_ref(), &query.token).await? else {
return Ok(HttpResponse::NotFound().finish());
};
return Ok(template.to_response());
}
let template = ResetPasswordTemplate {
token: &query.token,
title: "Brass - Registrierung",
endpoint: "/register",
new_password_label: "Passwort:",
retype_label: "Passwort wiederholen:",
submit_button_label: "Registrieren",
};
Ok(HttpResponse::NotFound().finish())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,11 +1,10 @@
use actix_identity::Identity;
use actix_web::{get, http::header::LOCATION, web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool;
use crate::models::PasswordReset;
use crate::{models::PasswordReset, utils::ApplicationError};
use super::ResetPasswordTemplate;
@ -23,13 +22,13 @@ pub async fn get(
user: Option<Identity>,
pool: web::Data<PgPool>,
query: web::Query<TokenQuery>,
) -> impl Responder {
) -> Result<impl Responder, ApplicationError> {
if let Some(_) = user {
return HttpResponse::Found()
return Ok(HttpResponse::Found()
.insert_header((LOCATION, "/"))
.finish();
.finish());
} else if let Some(token) = &query.token {
if let Ok(_) = PasswordReset::does_token_exist(pool.get_ref(), token).await {
if let Some(_) = PasswordReset::does_token_exist(pool.get_ref(), token).await? {
let template = ResetPasswordTemplate {
token,
title: "Brass - Passwort zurücksetzen",
@ -39,11 +38,10 @@ pub async fn get(
submit_button_label: "Passwort zurücksetzen",
};
return template.to_response();
return Ok(HttpResponse::Ok().body(template.render()?))
}
}
let template = ForgotPasswordTemplate {};
return template.to_response();
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -3,7 +3,7 @@ use crate::models::{Area, Role, Token, User};
use crate::utils::{password_help, ApplicationError};
use crate::{auth, filters};
use actix_web::HttpResponse;
use askama::Template;
use rinja::Template;
use sqlx::PgPool;
use zxcvbn::{zxcvbn, Score};
@ -73,7 +73,8 @@ async fn handle_password_change_request(
return Ok(no_message);
}
let user = User::read_by_id(pool, user_id).await?;
// TODO: make sure this unwrap is safe
let user = User::read_by_id(pool, user_id).await?.unwrap();
let mut split_names: Vec<&str> = user.name.as_str().split_whitespace().collect();
let mut user_inputs = vec![user.email.as_str()];
user_inputs.append(&mut split_names);

View File

@ -24,15 +24,16 @@ pub async fn post_edit(
path: web::Path<IdPath>,
form: web::Form<EditUserForm>,
) -> impl Responder {
// TODO: rewrite
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
.unwrap().unwrap();
if current_user.role != Role::AreaManager && current_user.role != Role::Admin {
return HttpResponse::Unauthorized().finish();
}
if let Ok(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await {
if let Some(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await.unwrap() {
if current_user.role == Role::AreaManager && current_user.area_id != user_in_db.area_id {
return HttpResponse::Unauthorized().finish();
}

View File

@ -48,7 +48,7 @@ pub async fn post_new(
.await?;
let registration = Registration::insert_new_for_user(pool.get_ref(), id).await?;
let new_user = User::read_by_id(pool.get_ref(), id).await?;
let new_user = User::read_by_id(pool.get_ref(), id).await?.unwrap();
let message = email::build_registration_message(&new_user, &registration.token)?;
mailer.send(&message)?;

View File

@ -19,12 +19,13 @@ pub async fn post(
path: web::Path<IdPath>,
query: web::Query<ToggleQuery>,
) -> impl Responder {
// Todo: rewrite
if user.id != path.id && user.role != Role::Admin && user.role != Role::AreaManager {
return HttpResponse::Unauthorized().finish();
}
let user = if user.id != path.id {
User::read_by_id(pool.get_ref(), path.id).await.unwrap()
User::read_by_id(pool.get_ref(), path.id).await.unwrap().unwrap()
} else {
user.into_inner()
};

View File

@ -1,5 +1,5 @@
use actix_web::{web, HttpResponse, Responder};
use askama_actix::TemplateToResponse;
use rinja::Template;
use sqlx::PgPool;
use crate::{
@ -27,5 +27,5 @@ pub async fn get(
vehicle: Some(vehicle),
};
Ok(template.to_response())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,5 +1,5 @@
use actix_web::{web, Responder};
use askama_actix::TemplateToResponse;
use actix_web::{web, HttpResponse, Responder};
use rinja::Template;
use crate::{
endpoints::vehicle::VehicleNewOrEditTemplate,
@ -18,5 +18,5 @@ pub async fn get(user: web::ReqData<User>) -> Result<impl Responder, Application
vehicle: None,
};
Ok(template.to_response())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,6 +1,5 @@
use actix_web::{web, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use actix_web::{web, HttpResponse, Responder};
use rinja::Template;
use sqlx::PgPool;
use crate::{models::{User, Vehicle, Role}, utils::ApplicationError};
@ -25,5 +24,5 @@ pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> Result<im
vehicles
};
Ok(template.to_response())
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1,4 +1,4 @@
use askama::Template;
use rinja::Template;
use serde::Deserialize;
use crate::models::{Role, User, Vehicle};

View File

@ -1,6 +1,6 @@
use crate::models::Function;
pub fn show_area_query(a: &Option<i32>, first: bool) -> ::askama::Result<String> {
pub fn show_area_query(a: &Option<i32>, first: bool) -> rinja::Result<String> {
let char = if first { '?' } else { '&' };
if let Some(a) = a {
@ -10,7 +10,7 @@ pub fn show_area_query(a: &Option<i32>, first: bool) -> ::askama::Result<String>
}
}
pub fn cond_show(show: &bool, text: &str) -> askama::Result<String> {
pub fn cond_show(show: &bool, text: &str) -> rinja::Result<String> {
return if *show {
Ok(String::from(text))
} else {
@ -18,7 +18,7 @@ pub fn cond_show(show: &bool, text: &str) -> askama::Result<String> {
};
}
pub fn insert_value(option: &Option<String>) -> askama::Result<String> {
pub fn insert_value(option: &Option<String>) -> rinja::Result<String> {
if let Some(val) = option {
let s = format!(r#"value="{val}""#);
return Ok(s);
@ -27,18 +27,18 @@ pub fn insert_value(option: &Option<String>) -> askama::Result<String> {
Ok(String::new())
}
pub fn is_some_and_eq<T>(option: &Option<T>, other: &T) -> askama::Result<bool>
pub fn is_some_and_eq<T>(option: &Option<T>, other: &T) -> rinja::Result<bool>
where
T: Eq,
{
Ok(option.as_ref().is_some_and(|x| x == other))
}
pub fn invert(b: &bool) -> askama::Result<bool> {
pub fn invert(b: &bool) -> rinja::Result<bool> {
return Ok(!b);
}
pub fn show_tree(f: &Function) -> askama::Result<String> {
pub fn show_tree(f: &Function) -> rinja::Result<String> {
let mut tags = String::from(r#"<span class="tag is-info is-light">Posten</span>"#);
if f == &Function::Fuehrungsassistent || f == &Function::Wachhabender {

View File

@ -56,6 +56,7 @@ where
let user = User::read_by_id(pool.get_ref(), id.parse().unwrap())
.await
.unwrap()
.unwrap();
req.extensions_mut().insert::<User>(user);

View File

@ -76,7 +76,7 @@ impl User {
b
}
pub async fn read_by_id(pool: &PgPool, id: i32) -> Result<User> {
pub async fn read_by_id(pool: &PgPool, id: i32) -> Result<Option<User>> {
let record = sqlx::query!(
r#"
SELECT id,
@ -95,23 +95,23 @@ impl User {
"#,
id,
)
.fetch_one(pool)
.fetch_optional(pool)
.await?;
let user = User {
id: record.id,
name: record.name,
email: record.email,
password: record.password,
salt: record.salt,
role: record.role,
function: record.function,
area_id: record.areaid,
let user = record.and_then(|u| Some(User {
id: u.id,
name: u.name,
email: u.email,
password: u.password,
salt: u.salt,
role: u.role,
function: u.function,
area_id: u.areaid,
area: None,
locked: record.locked,
last_login: record.lastlogin,
receive_notifications: record.receivenotifications,
};
locked: u.locked,
last_login: u.lastlogin,
receive_notifications: u.receivenotifications,
}));
Ok(user)
}
@ -197,7 +197,7 @@ impl User {
Ok(result)
}
pub async fn read_all_including_area(pool: &PgPool) -> anyhow::Result<Vec<User>> {
pub async fn read_all_including_area(pool: &PgPool) -> Result<Vec<User>> {
let records = sqlx::query!(
r#"
SELECT
@ -245,7 +245,7 @@ impl User {
Ok(results)
}
pub async fn read_all_by_area(pool: &PgPool, area_id: i32) -> anyhow::Result<Vec<User>> {
pub async fn read_all_by_area(pool: &PgPool, area_id: i32) -> Result<Vec<User>> {
let records = sqlx::query!(
r#"
SELECT id,

View File

@ -18,7 +18,9 @@ pub enum ApplicationError {
#[error("email transport not working")]
EmailTransport(#[from] lettre::transport::smtp::Error),
#[error("hashfunction failed")]
Hash(#[from] argon2::password_hash::Error)
Hash(#[from] argon2::password_hash::Error),
#[error("templating failed")]
Template(#[from] rinja::Error),
}
impl actix_web::error::ResponseError for ApplicationError {
@ -32,6 +34,7 @@ impl actix_web::error::ResponseError for ApplicationError {
ApplicationError::Email(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApplicationError::EmailTransport(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApplicationError::Hash(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApplicationError::Template(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
@ -43,6 +46,7 @@ impl actix_web::error::ResponseError for ApplicationError {
ApplicationError::EmailAdress(e) => response.body(format!("{self} - {e}")),
ApplicationError::Email(e) => response.body(format!("{self} - {e}")),
ApplicationError::EmailTransport(e) => response.body(format!("{self} - {e}")),
ApplicationError::Template(e) => response.body(format!("{self} - {e}")),
_ => response.body(self.to_string()),
}
}

View File

@ -45,9 +45,9 @@
</div>
<div class="control">
<a class="button is-link is-light" hx-boost="true" href="/locations">
<svg class="icon">
<use href="/static/feather-sprite.svg#arrow-left" />
</svg>
<svg class="icon">
<use href="/static/feather-sprite.svg#arrow-left" />
</svg>
<span>Zurück</span>
</a>
</div>
@ -58,7 +58,4 @@
</form>
</div>
</section>
<script>
</script>
{% endblock %}

View File

@ -32,10 +32,10 @@
<label class="radio">
{% if id.is_some() %}
<input type="radio" name="hasTime" hx-get="/availabillity/edit/{{ id.unwrap() }}?wholeday=false"
hx-target="closest body" {{ whole_day|invert|cond_show("checked") }} />
hx-target="closest body" {{ whole_day|invert|ref|cond_show("checked") }} />
{% else %}
<input type="radio" name="hasTime" hx-get="/availabillity/new?date={{ date }}&wholeday=false"
hx-target="closest body" {{ whole_day|invert|cond_show("checked") }} />
hx-target="closest body" {{ whole_day|invert|ref|cond_show("checked") }} />
{% endif %}
zeitweise
</label>
@ -51,11 +51,11 @@
<div class="field-body">
<div class="field">
<input class="input" type="time" id="from" name="from" value='{{ start_time.unwrap_or("00:00") }}' {{
whole_day|cond_show("disabled") }} {{ whole_day|invert|cond_show("required") }}>
whole_day|cond_show("disabled") }} {{ whole_day|invert|ref|cond_show("required") }}>
</div>
<div class="field">
<input class="input" type="time" id="till" name="till" value='{{ end_time.unwrap_or("23:59") }}' {{
whole_day|cond_show("disabled") }} {{ whole_day|invert|cond_show("required") }}>
whole_day|cond_show("disabled") }} {{ whole_day|invert|ref|cond_show("required") }}>
</div>
</div>
</div>

View File

@ -43,7 +43,7 @@
</div>
<div class="cell is-col-span-2">
<p><b>Anmerkungen:</b> {{ event.note.as_ref().unwrap_or(String::new()|as_ref) }}</p>
<p><b>Anmerkungen:</b> {{ event.note.as_ref().unwrap_or(String::new()|ref) }}</p>
</div>
</div>
</div>

View File

@ -45,23 +45,23 @@
<div class="buttons has-addons">
<button hx-post="/assignments/new?event={{ event.id }}&availabillity={{ availabillity.id }}&function=1"
hx-target="closest table" hx-swap="outerHTML" class="button is-small" {% if !further_posten_required ||
status !=AvailabillityAssignmentState::Unassigned|as_ref %}disabled{% endif %}>Posten</button>
status !=AvailabillityAssignmentState::Unassigned|ref %}disabled{% endif %}>Posten</button>
{% if u.function == Function::Wachhabender || u.function == Function::Fuehrungsassistent %}
<button hx-post="/assignments/new?event={{ event.id }}&availabillity={{ availabillity.id }}&function=5"
hx-target="closest table" hx-swap="outerHTML" class="button is-small" {% if
!further_fuehrungsassistent_required || status !=AvailabillityAssignmentState::Unassigned|as_ref
!further_fuehrungsassistent_required || status !=AvailabillityAssignmentState::Unassigned|ref
%}disabled{% endif %}>Führungsassistent</button>
{% endif %}
{% if u.function == Function::Wachhabender %}
<button hx-post="/assignments/new?event={{ event.id }}&availabillity={{ availabillity.id }}&function=10"
hx-target="closest table" hx-swap="outerHTML" class="button is-small" {% if !further_wachhabender_required
|| status !=AvailabillityAssignmentState::Unassigned|as_ref %}disabled{% endif %}>Wachhabender</button>
|| status !=AvailabillityAssignmentState::Unassigned|ref %}disabled{% endif %}>Wachhabender</button>
{% endif %}
</div>
</td>
<td>
{% if status != AvailabillityAssignmentState::Unassigned|as_ref && status !=
AvailabillityAssignmentState::Conflicting|as_ref %}
{% if status != AvailabillityAssignmentState::Unassigned|ref && status !=
AvailabillityAssignmentState::Conflicting|ref %}
<button hx-delete="/assignments/delete?event={{ event.id }}&availabillity={{ availabillity.id }}"
hx-target="closest table" hx-swap="outerHTML" class="button is-small">Entplanen</button>
{% endif %}

View File

@ -40,7 +40,7 @@
{% let areaid = loc.area_id %}
{% endif -%}
{% for area in areas.as_ref().unwrap() %}
<option {{ areaid|is_some_and_eq(area.id)|cond_show("selected") }} value="{{ area.id }}">{{
<option {{ areaid|is_some_and_eq(area.id)|ref|cond_show("selected") }} value="{{ area.id }}">{{
area.name }}</option>
{% endfor %}
</select>

View File

@ -48,10 +48,10 @@
<div class="control">
<div class="select is-fullwidth">
<select name="role">
<option value="1" {{ role|is_some_and_eq(1|as_ref)|cond_show("selected") }}>Personal</option>
<option value="10" {{ role|is_some_and_eq(10|as_ref)|cond_show("selected") }}>Bereichsleiter
<option value="1" {{ role|is_some_and_eq(1|ref)|ref|cond_show("selected") }}>Personal</option>
<option value="10" {{ role|is_some_and_eq(10|ref)|ref|cond_show("selected") }}>Bereichsleiter
</option>
<option value="100" {{ role|is_some_and_eq(100|as_ref)|cond_show("selected") }}>Admin</option>
<option value="100" {{ role|is_some_and_eq(100|ref)|ref|cond_show("selected") }}>Admin</option>
</select>
</div>
</div>
@ -68,10 +68,10 @@
<div class="control">
<div class="select is-fullwidth">
<select name="function">
<option value="1" {{ function|is_some_and_eq(1|as_ref)|cond_show("selected") }}>Posten</option>
<option value="1" {{ function|is_some_and_eq(5|as_ref)|cond_show("selected") }}>Führungsassistent
<option value="1" {{ function|is_some_and_eq(1|ref)|ref|cond_show("selected") }}>Posten</option>
<option value="1" {{ function|is_some_and_eq(5|ref)|ref|cond_show("selected") }}>Führungsassistent
</option>
<option value="10" {{ function|is_some_and_eq(10|as_ref)|cond_show("selected") }}>Wachhabender
<option value="10" {{ function|is_some_and_eq(10|ref)|ref|cond_show("selected") }}>Wachhabender
</option>
</select>
</div>
@ -91,7 +91,7 @@
<div class="select is-fullwidth">
<select name="area">
{% for area in areas.as_ref().unwrap() %}
<option value="{{ area.id }}" {{ area_id|is_some_and_eq(area.id)|cond_show("selected") }}>{{
<option value="{{ area.id }}" {{ area_id|is_some_and_eq(area.id)|ref|cond_show("selected") }}>{{
area.name }}</option>
{% endfor %}
</select>