From 649d1a6ecf7ae58dcf5559ab4a206f5b2efe876a Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Sun, 13 Jul 2025 10:45:56 +0200 Subject: [PATCH] refactor: use hx-target header for password strength check request --- web/src/endpoints/user/post_changepassword.rs | 17 ++++--- web/src/endpoints/user/post_register.rs | 4 +- web/src/endpoints/user/post_reset.rs | 4 +- web/src/utils/application_error.rs | 8 ---- web/src/utils/htmx_target_header.rs | 47 +++++++++++++++++++ web/src/utils/mod.rs | 2 + web/src/utils/password_change/mod.rs | 37 ++++++++++++--- .../utils/password_change/password_change.rs | 4 +- web/templates/user/change_password.html | 14 +++--- .../user/profile_change_password.html | 15 +++--- 10 files changed, 108 insertions(+), 44 deletions(-) create mode 100644 web/src/utils/htmx_target_header.rs diff --git a/web/src/endpoints/user/post_changepassword.rs b/web/src/endpoints/user/post_changepassword.rs index f62503d0..0665d082 100644 --- a/web/src/endpoints/user/post_changepassword.rs +++ b/web/src/endpoints/user/post_changepassword.rs @@ -3,7 +3,7 @@ use maud::html; use serde::Deserialize; use sqlx::PgPool; -use crate::utils::{password_change::PasswordChangeBuilder, ApplicationError}; +use crate::utils::{password_change::PasswordChangeBuilder, ApplicationError, HtmxTargetHeader}; use brass_db::{models::User, NoneToken}; #[derive(Deserialize)] @@ -11,17 +11,16 @@ struct ChangePasswordForm { currentpassword: String, password: String, passwordretyped: String, - dry: Option, } #[actix_web::post("/users/changepassword")] async fn post( user: web::ReqData, + header: web::Header, form: web::Form, pool: web::Data, ) -> Result { - // TODO: refactor into check if HX-TARGET = #password-strength exists - let is_dry = form.dry.unwrap_or(false); + let is_dry = header.into_inner().is_some_and_equal("password-strength"); let mut builder = PasswordChangeBuilder::::new( pool.get_ref(), @@ -34,9 +33,15 @@ async fn post( let change = builder.build(); let response = if is_dry { - change.validate_for_input().await? + match change.validate_for_input().await { + Ok(r) => r, + Err(e) => HttpResponse::UnprocessableEntity().body(e.message), + } } else { - change.validate().await?; + if let Err(e) = change.validate().await { + return Ok(HttpResponse::UnprocessableEntity().body(e.message)); + } + change.commit().await?; HttpResponse::Ok().body( html! { diff --git a/web/src/endpoints/user/post_register.rs b/web/src/endpoints/user/post_register.rs index 39dc1aaf..f1cff3e8 100644 --- a/web/src/endpoints/user/post_register.rs +++ b/web/src/endpoints/user/post_register.rs @@ -39,9 +39,9 @@ async fn post( let change = builder.build(); let response = if is_dry { - change.validate_for_input().await? + change.validate_for_input().await.unwrap() // TODO } else { - change.validate().await?; + change.validate().await.unwrap(); // TODO change.commit().await?; HttpResponse::Ok().body( html! { diff --git a/web/src/endpoints/user/post_reset.rs b/web/src/endpoints/user/post_reset.rs index 57539c78..0f91c246 100644 --- a/web/src/endpoints/user/post_reset.rs +++ b/web/src/endpoints/user/post_reset.rs @@ -66,9 +66,9 @@ async fn post( let change = builder.build(); let response = if is_dry { - change.validate_for_input().await? + change.validate_for_input().await.unwrap() // TODO: } else { - change.validate().await?; + change.validate().await.unwrap(); // TODO change.commit().await?; HttpResponse::Ok().body( html! { diff --git a/web/src/utils/application_error.rs b/web/src/utils/application_error.rs index 0d2a8f2f..fd3100c1 100644 --- a/web/src/utils/application_error.rs +++ b/web/src/utils/application_error.rs @@ -1,8 +1,6 @@ use actix_web::{http::StatusCode, HttpResponse}; use thiserror::Error; -use super::password_change::PasswordChangeError; - #[derive(Debug, Error)] pub enum ApplicationError { #[error("unsupported value for enum")] @@ -25,11 +23,6 @@ pub enum ApplicationError { Hash(#[from] argon2::password_hash::Error), #[error("templating failed")] Template(#[from] askama::Error), - #[error("{}", inner.message)] - PasswordChange { - #[from] - inner: PasswordChangeError, - }, } impl actix_web::error::ResponseError for ApplicationError { @@ -45,7 +38,6 @@ impl actix_web::error::ResponseError for ApplicationError { ApplicationError::Hash(_) => StatusCode::INTERNAL_SERVER_ERROR, ApplicationError::Template(_) => StatusCode::INTERNAL_SERVER_ERROR, ApplicationError::EmailStubTransport(_) => StatusCode::INTERNAL_SERVER_ERROR, - ApplicationError::PasswordChange { .. } => StatusCode::BAD_REQUEST, } } diff --git a/web/src/utils/htmx_target_header.rs b/web/src/utils/htmx_target_header.rs new file mode 100644 index 00000000..199a32a0 --- /dev/null +++ b/web/src/utils/htmx_target_header.rs @@ -0,0 +1,47 @@ +use std::str::FromStr; + +use actix_http::header::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue}; +use actix_web::error::ParseError; + +#[derive(Debug)] +pub struct HtmxTargetHeader(Option); + +impl Header for HtmxTargetHeader { + fn name() -> HeaderName { + HeaderName::from_str("HX-Target").unwrap() + } + + fn parse(msg: &M) -> Result { + let header = msg.headers().get(Self::name()); + if let Some(line) = header { + let line = line.to_str().map_err(|_| ParseError::Header)?; + + if line.is_empty() { + return Err(ParseError::Header); + } + + return Ok(self::HtmxTargetHeader(Some(line.to_string()))); + } else { + Ok(self::HtmxTargetHeader(None)) + } + } +} + +impl TryIntoHeaderValue for HtmxTargetHeader { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into_value(self) -> Result { + if let Some(s) = self.0 { + return s.try_into_value(); + } else { + HeaderValue::from_str("") + } + } +} + +impl HtmxTargetHeader { + pub fn is_some_and_equal(&self, target: &str) -> bool { + self.0.as_ref().is_some_and(|t| t == target) + } +} diff --git a/web/src/utils/mod.rs b/web/src/utils/mod.rs index f0957163..a6ef56ca 100644 --- a/web/src/utils/mod.rs +++ b/web/src/utils/mod.rs @@ -3,6 +3,7 @@ mod application_error; pub mod auth; mod date_time_format; pub mod event_planning_template; +mod htmx_target_header; pub mod manage_commands; pub mod password_change; mod template_response_trait; @@ -13,6 +14,7 @@ pub mod test_helper; pub use app_customization::Customization; pub use application_error::ApplicationError; pub use date_time_format::DateTimeFormat; +pub use htmx_target_header::HtmxTargetHeader; pub use template_response_trait::TemplateResponse; use chrono::{NaiveDate, Utc}; diff --git a/web/src/utils/password_change/mod.rs b/web/src/utils/password_change/mod.rs index 95d25404..9428913b 100644 --- a/web/src/utils/password_change/mod.rs +++ b/web/src/utils/password_change/mod.rs @@ -1,5 +1,6 @@ use maud::html; use thiserror::Error; +use tracing::error; use zxcvbn::{ feedback::{Suggestion, Warning}, Entropy, @@ -17,6 +18,28 @@ pub struct PasswordChangeError { pub message: String, } +impl PasswordChangeError { + pub fn new(message: &str) -> Self { + PasswordChangeError { + message: message.to_string(), + } + } +} + +impl From for PasswordChangeError { + fn from(value: sqlx::Error) -> Self { + error!(error = %value, "database error while validation input"); + Self::new("Datenbankfehler beim Validieren!") + } +} + +impl From for PasswordChangeError { + fn from(value: argon2::password_hash::Error) -> Self { + error!(error = %value, "argon2 hash error while validation input"); + Self::new("Hashingfehler beim Validieren!") + } +} + fn generate_help_message_for_entropy(entropy: &Entropy) -> String { let feedback = entropy.feedback().unwrap(); @@ -62,11 +85,11 @@ fn generate_help_message_for_entropy(entropy: &Entropy) -> String { "Vorschlag" }; - let suggestion = feedback + let suggestions = feedback .suggestions() .iter() .map(|s| { - let inner = match s { + match s { Suggestion::UseAFewWordsAvoidCommonPhrases => "Mehrere Wörter verwenden, aber allgemeine Phrasen vermeiden.", Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters => "Es ist möglich, starke Passwörter zu erstellen, ohne Symbole, Zahlen oder Großbuchstaben zu verwenden.", Suggestion::AddAnotherWordOrTwo => "Weitere Wörter, die weniger häufig vorkommen, hinzufügen.", @@ -80,12 +103,10 @@ fn generate_help_message_for_entropy(entropy: &Entropy) -> String { Suggestion::AvoidRecentYears => "Die jüngsten Jahreszahlen vermeiden.", Suggestion::AvoidYearsThatAreAssociatedWithYou => "Jahre, die mit persönlichen Daten in Verbindung gebracht werden können, vermeiden.", Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou => "Daten, die mit persönlichen Daten in Verbindung gebracht werden können, vermeiden.", - }; + } - format!("
  • {inner}
  • ") }) - .collect::>() - .join(""); + .collect::>(); html!( { @@ -93,7 +114,9 @@ fn generate_help_message_for_entropy(entropy: &Entropy) -> String { p { ( warning )} (vorschlag_text) ":" ul { - (suggestion) + @for s in &suggestions { + li { (s) } + } } } diff --git a/web/src/utils/password_change/password_change.rs b/web/src/utils/password_change/password_change.rs index 8ced8dc9..1cc3d046 100644 --- a/web/src/utils/password_change/password_change.rs +++ b/web/src/utils/password_change/password_change.rs @@ -28,7 +28,7 @@ where T: Token, { /// should be called after password input has changed to hint the user of any input related problems - pub async fn validate_for_input(&self) -> Result { + pub async fn validate_for_input(&self) -> Result { if self.password.chars().count() > 256 { return Err(PasswordChangeError { message: html! { @@ -68,7 +68,7 @@ where } /// should be called after the form is fully filled and submit is clicked - pub async fn validate(&self) -> Result<(), ApplicationError> { + pub async fn validate(&self) -> Result<(), PasswordChangeError> { self.validate_for_input().await?; if let Some(current_password) = self.current_password { diff --git a/web/templates/user/change_password.html b/web/templates/user/change_password.html index fd196f11..9ea55619 100644 --- a/web/templates/user/change_password.html +++ b/web/templates/user/change_password.html @@ -4,19 +4,17 @@

    {{ title }}

    -
    + - -
    - +
    diff --git a/web/templates/user/profile_change_password.html b/web/templates/user/profile_change_password.html index e03e4b66..84e397ca 100644 --- a/web/templates/user/profile_change_password.html +++ b/web/templates/user/profile_change_password.html @@ -3,10 +3,8 @@ Schließen
    - - - +
    @@ -18,12 +16,11 @@
    - +
    -
    +