refactor: use hx-target header for password strength check request
This commit is contained in:
parent
28cc3cd9ae
commit
649d1a6ecf
@ -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<bool>,
|
||||
}
|
||||
|
||||
#[actix_web::post("/users/changepassword")]
|
||||
async fn post(
|
||||
user: web::ReqData<User>,
|
||||
header: web::Header<HtmxTargetHeader>,
|
||||
form: web::Form<ChangePasswordForm>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<impl Responder, ApplicationError> {
|
||||
// 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::<NoneToken>::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! {
|
||||
|
@ -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! {
|
||||
|
@ -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! {
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
47
web/src/utils/htmx_target_header.rs
Normal file
47
web/src/utils/htmx_target_header.rs
Normal file
@ -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<String>);
|
||||
|
||||
impl Header for HtmxTargetHeader {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::from_str("HX-Target").unwrap()
|
||||
}
|
||||
|
||||
fn parse<M: actix_web::HttpMessage>(msg: &M) -> Result<Self, actix_web::error::ParseError> {
|
||||
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<HeaderValue, Self::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
@ -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};
|
||||
|
@ -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<sqlx::Error> for PasswordChangeError {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
error!(error = %value, "database error while validation input");
|
||||
Self::new("Datenbankfehler beim Validieren!")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<argon2::password_hash::Error> 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!("<li>{inner}</li>")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<HttpResponse, ApplicationError> {
|
||||
pub async fn validate_for_input(&self) -> Result<HttpResponse, PasswordChangeError> {
|
||||
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 {
|
||||
|
@ -4,19 +4,17 @@
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">{{ title }}</h1>
|
||||
<form class="box" hx-post="{{ endpoint }}" hx-params="not dry" hx-target-400="#error-message-retype"
|
||||
hx-on:input="document.getElementById('error-message-retype').innerHTML = ''">
|
||||
<form class="box" hx-post="{{ endpoint }}" hx-target-422="#error-message-retype"
|
||||
_="on input put '' into #error-message-retype">
|
||||
<input type="hidden" name="token" value="{{ token }}" />
|
||||
|
||||
<input type="hidden" name="dry" value="true" />
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="password">{{ new_password_label }}</label>
|
||||
<div class="control">
|
||||
<input class="input" hx-post="{{ endpoint }}" hx-params="*" hx-trigger="keyup changed delay:500ms"
|
||||
hx-target="#password-strength" hx-target-400="#password-strength" placeholder="**********" name="password"
|
||||
type="password" required hx-swap="outerHTML" maxlength=256
|
||||
hx-on:input="document.getElementById('password-strength').innerHTML = ''">
|
||||
<input class="input" hx-trigger="keyup changed delay:500ms" hx-target="#password-strength"
|
||||
hx-target-422="#password-strength" placeholder="**********" name="password"
|
||||
type="password" required hx-swap="outerHTML" maxlength=256
|
||||
_="on input put '' into #password-strength">
|
||||
</div>
|
||||
<div id="password-strength" class="mb-3 help content"></div>
|
||||
</div>
|
||||
|
@ -3,10 +3,8 @@
|
||||
<a href="/profile" hx-boost="true" class="button is-link is-light">Schließen</a>
|
||||
</div>
|
||||
</div>
|
||||
<form class="box" hx-post="/users/changepassword" hx-params="not dry" hx-target-400="#error-message"
|
||||
hx-on:change="document.getElementById('error-message').innerHTML = ''">
|
||||
|
||||
<input type="hidden" name="dry" value="true" />
|
||||
<form class="box" hx-post="/users/changepassword" hx-target-422="#error-message"
|
||||
_="on change put '' into #error-message">
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="currentpassword">aktuelles Passwort:</label>
|
||||
@ -18,12 +16,11 @@
|
||||
<div class="field">
|
||||
<label class="label" for="password">neues Passwort:</label>
|
||||
<div class="control">
|
||||
<input class="input" hx-post="/users/changepassword" hx-params="*" hx-trigger="keyup changed delay:500ms"
|
||||
hx-target="#password-strength" hx-target-400="#password-strength" placeholder="**********" name="password"
|
||||
type="password" required hx-swap="outerHTML" maxlength=256
|
||||
hx-on:input="document.getElementById('password-strength').innerHTML = ''">
|
||||
<input class="input" hx-trigger="keyup changed delay:500ms" hx-target="#password-strength"
|
||||
hx-post="/users/changepassword" hx-target-422="#password-strength" placeholder="**********" name="password"
|
||||
type="password" required hx-swap="outerHTML" maxlength=256 _="on input put '' into #password-strength">
|
||||
</div>
|
||||
<div id="password-strength" class="mb-3 help content"></div>
|
||||
<div id="password-strength" class=" mb-3 help content"></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
|
Loading…
x
Reference in New Issue
Block a user