refactor: clippy lints

This commit is contained in:
Max Hohlfeld 2025-07-14 16:17:43 +02:00
parent bddeaefe4f
commit 2aa3cf2c2b
22 changed files with 287 additions and 283 deletions

View File

@ -295,15 +295,13 @@ impl Availability {
.fetch_all(pool) // possible to find up to two availabilities (upper and lower), for now we only pick one and extend it .fetch_all(pool) // possible to find up to two availabilities (upper and lower), for now we only pick one and extend it
.await?; .await?;
let adjacent_avaialability = records.first().and_then(|r| { let adjacent_avaialability = records.first().map(|r| Availability {
Some(Availability { id: r.id,
id: r.id, user_id: r.userid,
user_id: r.userid, user: None,
user: None, start: r.starttimestamp.naive_utc(),
start: r.starttimestamp.naive_utc(), end: r.endtimestamp.naive_utc(),
end: r.endtimestamp.naive_utc(), comment: r.comment.clone(),
comment: r.comment.clone(),
})
}); });
Ok(adjacent_avaialability) Ok(adjacent_avaialability)

View File

@ -26,17 +26,17 @@ impl<'a> AsyncValidate<'a> for AvailabilityChangeset {
&self, &self,
context: &'a Self::Context, context: &'a Self::Context,
) -> Result<(), AsyncValidateError> { ) -> Result<(), AsyncValidateError> {
let mut existing_availabilities = let mut existing_availabilities = Availability::read_all_by_user_and_date(
Availability::read_all_by_user_and_date(context.pool, context.user_id, &self.time.0.date()) context.pool,
.await?; context.user_id,
&self.time.0.date(),
)
.await?;
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?; start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
if let Some(availability) = context.availability { if let Some(availability) = context.availability {
existing_availabilities = existing_availabilities existing_availabilities.retain(|a| a.id != availability);
.into_iter()
.filter(|a| a.id != availability)
.collect();
time_is_not_already_assigned(&self.time, availability, context.pool).await?; time_is_not_already_assigned(&self.time, availability, context.pool).await?;
} }
@ -51,7 +51,7 @@ impl<'a> AsyncValidate<'a> for AvailabilityChangeset {
fn time_is_not_already_made_available( fn time_is_not_already_made_available(
(start, end): &(NaiveDateTime, NaiveDateTime), (start, end): &(NaiveDateTime, NaiveDateTime),
existing_availabilities: &Vec<Availability>, existing_availabilities: &[Availability],
) -> Result<(), AsyncValidateError> { ) -> Result<(), AsyncValidateError> {
let free_slots = find_free_date_time_slots(existing_availabilities); let free_slots = find_free_date_time_slots(existing_availabilities);
@ -65,7 +65,7 @@ fn time_is_not_already_made_available(
let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= *end && s.1 >= *end); let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= *end && s.1 >= *end);
let is_already_present_as_is = existing_availabilities let is_already_present_as_is = existing_availabilities
.iter() .iter()
.any(|a| a.start == *start && a.end == a.end); .any(|a| a.start == *start && a.end == *end);
if !free_block_found_for_start || !free_block_found_for_end || is_already_present_as_is { if !free_block_found_for_start || !free_block_found_for_end || is_already_present_as_is {
return Err(AsyncValidateError::new( return Err(AsyncValidateError::new(

View File

@ -117,7 +117,7 @@ fn date_unchanged_if_edit(
async fn time_can_be_extended_if_edit( async fn time_can_be_extended_if_edit(
time: &(NaiveDateTime, NaiveDateTime), time: &(NaiveDateTime, NaiveDateTime),
event: &Event, event: &Event,
assignments_for_event: &Vec<Assignment>, assignments_for_event: &[Assignment],
pool: &PgPool, pool: &PgPool,
) -> Result<(), AsyncValidateError> { ) -> Result<(), AsyncValidateError> {
let start = event.start.date(); let start = event.start.date();
@ -197,7 +197,7 @@ async fn time_can_be_extended_if_edit(
fn can_unset_fuehrungsassistent( fn can_unset_fuehrungsassistent(
fuehrungsassistent_required: &bool, fuehrungsassistent_required: &bool,
assignments_for_event: &Vec<Assignment>, assignments_for_event: &[Assignment],
) -> Result<(), AsyncValidateError> { ) -> Result<(), AsyncValidateError> {
if !*fuehrungsassistent_required if !*fuehrungsassistent_required
&& assignments_for_event && assignments_for_event
@ -214,7 +214,7 @@ fn can_unset_fuehrungsassistent(
fn can_unset_wachhabender( fn can_unset_wachhabender(
voluntary_wachhabender: &bool, voluntary_wachhabender: &bool,
assignments_for_event: &Vec<Assignment>, assignments_for_event: &[Assignment],
) -> Result<(), AsyncValidateError> { ) -> Result<(), AsyncValidateError> {
if !*voluntary_wachhabender if !*voluntary_wachhabender
&& assignments_for_event && assignments_for_event

View File

@ -57,8 +57,8 @@ impl ExportEventRow {
voluntary_wachhabender: r.voluntarywachhabender, voluntary_wachhabender: r.voluntarywachhabender,
location_name: r.locationname, location_name: r.locationname,
event_name: r.eventname, event_name: r.eventname,
assignments: r.assignments.unwrap_or(Vec::new()), assignments: r.assignments.unwrap_or_default(),
vehicles: r.vehicles.unwrap_or(Vec::new()), vehicles: r.vehicles.unwrap_or_default(),
}) })
.collect(); .collect();

View File

@ -152,7 +152,7 @@ impl User {
.fetch_optional(pool) .fetch_optional(pool)
.await?; .await?;
Ok(record.and_then(|r| Some(r.id))) Ok(record.map(|r| r.id))
} }
pub async fn read_all(pool: &PgPool) -> Result<Vec<User>> { pub async fn read_all(pool: &PgPool) -> Result<Vec<User>> {

View File

@ -19,7 +19,7 @@ impl Display for UserFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut iterator = self.0.iter().peekable(); let mut iterator = self.0.iter().peekable();
while let Some(p) = iterator.next() { while let Some(p) = iterator.next() {
write!(f, "{}", p.to_string())?; write!(f, "{p}")?;
if iterator.peek().is_some() { if iterator.peek().is_some() {
write!(f, ", ")?; write!(f, ", ")?;
} }

View File

@ -60,13 +60,15 @@ mod short_date_time_format {
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use serde::{self, Deserialize, Deserializer}; use serde::{self, Deserialize, Deserializer};
use crate::utils::DateTimeFormat;
pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDateTime, D::Error> pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDateTime, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
const FORMAT: &'static str = "%Y-%m-%dT%H:%M"; let format = DateTimeFormat::YearMonthDayTHourMinute.into();
let s = String::deserialize(deserializer)?; let s = String::deserialize(deserializer)?;
let dt = NaiveDateTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)?; let dt = NaiveDateTime::parse_from_str(&s, format).map_err(serde::de::Error::custom)?;
Ok(dt) Ok(dt)
} }
} }

View File

@ -61,8 +61,12 @@ pub async fn get(
user.area_id user.area_id
}; };
let availabilities = let availabilities = Availability::read_all_by_daterange_and_area_including_user_for_export(
Availability::read_all_by_daterange_and_area_including_user_for_export(pool.get_ref(), (start_date, end_date), area_id).await?; pool.get_ref(),
(start_date, end_date),
area_id,
)
.await?;
let export_availabilities = availabilities let export_availabilities = availabilities
.into_iter() .into_iter()
@ -73,7 +77,7 @@ pub async fn get(
start: a.start, start: a.start,
end: a.end, end: a.end,
assigned: false, assigned: false,
comment: a.comment.unwrap_or(String::new()), comment: a.comment.unwrap_or_default(),
}) })
.collect(); .collect();

View File

@ -55,8 +55,8 @@ fn read(rows: Vec<ExportEventRow>) -> Vec<EventExportEntry> {
hours: (r.end_timestamp - r.start_timestamp).as_seconds_f32() / 3600.0 + 1.0, hours: (r.end_timestamp - r.start_timestamp).as_seconds_f32() / 3600.0 + 1.0,
location: r.location_name.to_string(), location: r.location_name.to_string(),
name: r.event_name.to_string(), name: r.event_name.to_string(),
assigned_name: n.and_then(|s| Some(s.to_string())), assigned_name: n.map(|s| s.to_string()),
assigned_function: f.and_then(|s| Some(s.to_string())), assigned_function: f.map(|s| s.to_string()),
}; };
if let Some(assigned_wh) = r if let Some(assigned_wh) = r
@ -182,11 +182,11 @@ pub async fn get(
let buffer = workbook.save_to_buffer().unwrap(); let buffer = workbook.save_to_buffer().unwrap();
return Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.content_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") .content_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
.insert_header(( .insert_header((
CONTENT_DISPOSITION, CONTENT_DISPOSITION,
ContentDisposition::attachment("export.xlsx"), ContentDisposition::attachment("export.xlsx"),
)) ))
.body(buffer)); .body(buffer))
} }

View File

@ -34,7 +34,10 @@ pub async fn get(
} }
if let Some(token) = &query.token { if let Some(token) = &query.token {
if let Some(_) = PasswordReset::does_token_exist(pool.get_ref(), token).await? { if PasswordReset::does_token_exist(pool.get_ref(), token)
.await?
.is_some()
{
let template = ResetPasswordTemplate { let template = ResetPasswordTemplate {
token, token,
title: "Brass - Passwort zurücksetzen", title: "Brass - Passwort zurücksetzen",

View File

@ -51,7 +51,10 @@ pub async fn post_new(
area_id, area_id,
}; };
if let Some(_) = User::exists(pool.get_ref(), &changeset.email).await? { if User::exists(pool.get_ref(), &changeset.email)
.await?
.is_some()
{
return Ok(HttpResponse::UnprocessableEntity() return Ok(HttpResponse::UnprocessableEntity()
.body("email: an user already exists with the same email")); .body("email: an user already exists with the same email"));
} }

View File

@ -39,7 +39,7 @@ async fn post(
.await?; .await?;
} }
return Ok(HttpResponse::Ok().body("E-Mail versandt!")); Ok(HttpResponse::Ok().body("E-Mail versandt!"))
} }
Either::Right(form) => { Either::Right(form) => {
let is_dry = header.is_some_and_equal("password-strength"); let is_dry = header.is_some_and_equal("password-strength");
@ -83,7 +83,7 @@ async fn post(
) )
}; };
return Ok(response); Ok(response)
} }
} }
} }

View File

@ -119,12 +119,10 @@ pub fn fmt_time(v: &NaiveTime, format: DateTimeFormat) -> askama::Result<String>
} }
fn escape_html(string: String) -> String { fn escape_html(string: String) -> String {
let s = string string
.replace('&', "&amp;") .replace('&', "&amp;")
.replace('<', "&lt;") .replace('<', "&lt;")
.replace('>', "&gt;") .replace('>', "&gt;")
.replace('"', "&quot;") .replace('"', "&quot;")
.replace('\'', "&#x27;"); .replace('\'', "&#x27;")
s
} }

View File

@ -58,7 +58,7 @@ fn build(
let sender_mailbox = Mailbox::new( let sender_mailbox = Mailbox::new(
Some("noreply".to_string()), Some("noreply".to_string()),
Address::new("noreply", &hostname)?, Address::new("noreply", hostname)?,
); );
let message = Message::builder() let message = Message::builder()

View File

@ -62,7 +62,7 @@ fn build(
let sender_mailbox = Mailbox::new( let sender_mailbox = Mailbox::new(
Some("noreply".to_string()), Some("noreply".to_string()),
Address::new("noreply", &hostname)?, Address::new("noreply", hostname)?,
); );
let message = Message::builder() let message = Message::builder()

View File

@ -28,8 +28,8 @@ pub enum ApplicationError {
impl actix_web::error::ResponseError for ApplicationError { impl actix_web::error::ResponseError for ApplicationError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match *self { match *self {
ApplicationError::UnsupportedEnumValue { .. } => StatusCode::BAD_REQUEST, ApplicationError::UnsupportedEnumValue(_) => StatusCode::BAD_REQUEST,
ApplicationError::Unauthorized { .. } => StatusCode::UNAUTHORIZED, ApplicationError::Unauthorized => StatusCode::UNAUTHORIZED,
ApplicationError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, ApplicationError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApplicationError::EnvVariable(_) => StatusCode::INTERNAL_SERVER_ERROR, ApplicationError::EnvVariable(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApplicationError::EmailAdress(_) => StatusCode::INTERNAL_SERVER_ERROR, ApplicationError::EmailAdress(_) => StatusCode::INTERNAL_SERVER_ERROR,

View File

@ -20,7 +20,7 @@ impl Header for HtmxTargetHeader {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
return Ok(self::HtmxTargetHeader(Some(line.to_string()))); Ok(self::HtmxTargetHeader(Some(line.to_string())))
} else { } else {
Ok(self::HtmxTargetHeader(None)) Ok(self::HtmxTargetHeader(None))
} }
@ -33,7 +33,7 @@ impl TryIntoHeaderValue for HtmxTargetHeader {
#[inline] #[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> { fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
if let Some(s) = self.0 { if let Some(s) = self.0 {
return s.try_into_value(); s.try_into_value()
} else { } else {
HeaderValue::from_str("") HeaderValue::from_str("")
} }

View File

@ -0,0 +1,195 @@
use actix_web::HttpResponse;
use maud::html;
use sqlx::PgPool;
use zxcvbn::{
feedback::{Suggestion, Warning},
zxcvbn, Entropy, Score,
};
use crate::utils::{
auth::{generate_salt_and_hash_plain_password, hash_plain_password_with_salt},
ApplicationError,
};
use brass_db::{models::User, Token};
use super::PasswordChangeError;
pub struct PasswordChange<'a, T>
where
T: Token,
{
pub(super) pool: &'a PgPool,
pub(super) user_id: i32,
pub(super) password: &'a str,
pub(super) password_retyped: &'a str,
pub(super) token: Option<T>,
pub(super) current_password: Option<&'a str>,
}
impl<T> PasswordChange<'_, T>
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, PasswordChangeError> {
if self.password.chars().count() > 256 {
return Err(PasswordChangeError {
message: html! {
div id="password-strength" class="mb-3 help content is-danger"{
"Password darf nicht länger als 256 Zeichen sein."
}
}
.into_string(),
});
}
// unwrap is safe, as either a token is present (tokens always require a userid present) or the current password is set (which requires a existing user)
let user = User::read_by_id(self.pool, self.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);
let entropy = zxcvbn(self.password, &user_inputs);
if entropy.score() < Score::Three {
let message = generate_help_message_for_entropy(&entropy);
return Err(PasswordChangeError { message });
}
Ok(HttpResponse::Ok().body(
html! {
div id="password-strength" class="mb-3 help content is-success" {
@if entropy.score() == Score::Three {
"Sicheres Passwort."
} @else {
"Sehr sicheres Passwort."
}
}
}
.into_string(),
))
}
/// should be called after the form is fully filled and submit is clicked
pub async fn validate(&self) -> Result<(), PasswordChangeError> {
self.validate_for_input().await?;
if let Some(current_password) = self.current_password {
// unwraps are safe, as login only works with password and salt and this call site requires login
let user = User::read_by_id(self.pool, self.user_id).await?.unwrap();
let hash = user.password.as_ref().unwrap();
let salt = user.salt.as_ref().unwrap();
if hash != &hash_plain_password_with_salt(current_password, salt)? {
return Err(PasswordChangeError {
message: "Aktuelles Passwort ist falsch.".to_string(),
});
}
}
if self.password != self.password_retyped {
return Err(PasswordChangeError {
message: "Passwörter stimmen nicht überein.".to_string(),
});
}
Ok(())
}
/// commits the password change to the database
pub async fn commit(&self) -> Result<(), ApplicationError> {
let (hash, salt) = generate_salt_and_hash_plain_password(self.password).unwrap();
User::update_password(self.pool, self.user_id, &hash, &salt).await?;
if let Some(token) = &self.token {
token.delete(self.pool).await?;
}
Ok(())
}
}
fn generate_help_message_for_entropy(entropy: &Entropy) -> String {
let feedback = entropy.feedback().unwrap();
let warning = match feedback.warning() {
Some(Warning::StraightRowsOfKeysAreEasyToGuess) => {
"Gerade Linien von Tasten auf der Tastatur sind leicht zu erraten."
}
Some(Warning::ShortKeyboardPatternsAreEasyToGuess) => {
"Kurze Tastaturmuster sind leicht zu erraten."
}
Some(Warning::RepeatsLikeAaaAreEasyToGuess) => {
"Sich wiederholende Zeichen wie 'aaa' sind leicht zu erraten."
}
Some(Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess) => {
"Sich wiederholende Zeichenmuster wie 'abcabcabc' sind leicht zu erraten."
}
Some(Warning::ThisIsATop10Password) => "Dies ist ein sehr häufig verwendetes Passwort.",
Some(Warning::ThisIsATop100Password) => "Dies ist ein häufig verwendetes Passwort.",
Some(Warning::ThisIsACommonPassword) => "Dies ist ein oft verwendetes Passwort.",
Some(Warning::ThisIsSimilarToACommonlyUsedPassword) => {
"Dieses Passwort weist Ähnlichkeit zu anderen, oft verwendeten Passwörtern auf."
}
Some(Warning::SequencesLikeAbcAreEasyToGuess) => {
"Häufige Zeichenfolgen wie 'abc' oder '1234' sind leicht zu erraten."
}
Some(Warning::RecentYearsAreEasyToGuess) => {
"Die jüngsten Jahreszahlen sind leicht zu erraten."
}
Some(Warning::AWordByItselfIsEasyToGuess) => "Einzelne Wörter sind leicht zu erraten.",
Some(Warning::DatesAreOftenEasyToGuess) => "Ein Datum ist leicht zu erraten.",
Some(Warning::NamesAndSurnamesByThemselvesAreEasyToGuess) => {
"Einzelne Namen oder Nachnamen sind leicht zu erraten."
}
Some(Warning::CommonNamesAndSurnamesAreEasyToGuess) => {
"Vornamen und Nachnamen sind leicht zu erraten."
}
_ => "Passwort ist zu schwach.",
};
let vorschlag_text = if feedback.suggestions().len() > 1 {
"Vorschläge"
} else {
"Vorschlag"
};
let suggestions = feedback
.suggestions()
.iter()
.map(|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.",
Suggestion::CapitalizationDoesntHelpVeryMuch => "Nicht nur den ersten Buchstaben groß schreiben.",
Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => "Einige, aber nicht alle Buchstaben groß schreiben.",
Suggestion::ReversedWordsArentMuchHarderToGuess => "Umgekehrte Schreibweise von gebräuchlichen Wörtern vermeiden.",
Suggestion::PredictableSubstitutionsDontHelpVeryMuch => "Vorhersehbare Buchstabenersetzungen wie '@' für 'a' vermeiden.",
Suggestion::UseALongerKeyboardPatternWithMoreTurns => "Längere Tastaturmuster in unterschiedlicher Tipprichtung verwenden.",
Suggestion::AvoidRepeatedWordsAndCharacters => "Wort- und Zeichenwiederholungen vermeiden.",
Suggestion::AvoidSequences => "Häufige Zeichenfolgen vermeiden.",
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.",
}
})
.collect::<Vec<&str>>();
html!(
{
div id="password-strength" class="mb-3 help content is-danger" {
p { ( warning )}
(vorschlag_text) ":"
ul {
@for s in &suggestions {
li { (s) }
}
}
}
}
)
.into_string()
}

View File

@ -0,0 +1,30 @@
use thiserror::Error;
use tracing::error;
#[derive(Debug, Error)]
#[error("{message}")]
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!")
}
}

View File

@ -1,126 +1,7 @@
use maud::html; mod builder;
use thiserror::Error; mod command;
use tracing::error; mod error;
use zxcvbn::{
feedback::{Suggestion, Warning},
Entropy,
};
mod password_change; pub use builder::PasswordChangeBuilder;
mod password_change_builder; pub use command::PasswordChange;
pub use error::PasswordChangeError;
pub use password_change::PasswordChange;
pub use password_change_builder::PasswordChangeBuilder;
#[derive(Debug, Error)]
#[error("{message}")]
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();
let warning = match feedback.warning() {
Some(Warning::StraightRowsOfKeysAreEasyToGuess) => {
"Gerade Linien von Tasten auf der Tastatur sind leicht zu erraten."
}
Some(Warning::ShortKeyboardPatternsAreEasyToGuess) => {
"Kurze Tastaturmuster sind leicht zu erraten."
}
Some(Warning::RepeatsLikeAaaAreEasyToGuess) => {
"Sich wiederholende Zeichen wie 'aaa' sind leicht zu erraten."
}
Some(Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess) => {
"Sich wiederholende Zeichenmuster wie 'abcabcabc' sind leicht zu erraten."
}
Some(Warning::ThisIsATop10Password) => "Dies ist ein sehr häufig verwendetes Passwort.",
Some(Warning::ThisIsATop100Password) => "Dies ist ein häufig verwendetes Passwort.",
Some(Warning::ThisIsACommonPassword) => "Dies ist ein oft verwendetes Passwort.",
Some(Warning::ThisIsSimilarToACommonlyUsedPassword) => {
"Dieses Passwort weist Ähnlichkeit zu anderen, oft verwendeten Passwörtern auf."
}
Some(Warning::SequencesLikeAbcAreEasyToGuess) => {
"Häufige Zeichenfolgen wie 'abc' oder '1234' sind leicht zu erraten."
}
Some(Warning::RecentYearsAreEasyToGuess) => {
"Die jüngsten Jahreszahlen sind leicht zu erraten."
}
Some(Warning::AWordByItselfIsEasyToGuess) => "Einzelne Wörter sind leicht zu erraten.",
Some(Warning::DatesAreOftenEasyToGuess) => "Ein Datum ist leicht zu erraten.",
Some(Warning::NamesAndSurnamesByThemselvesAreEasyToGuess) => {
"Einzelne Namen oder Nachnamen sind leicht zu erraten."
}
Some(Warning::CommonNamesAndSurnamesAreEasyToGuess) => {
"Vornamen und Nachnamen sind leicht zu erraten."
}
_ => "Passwort ist zu schwach.",
};
let vorschlag_text = if feedback.suggestions().len() > 1 {
"Vorschläge"
} else {
"Vorschlag"
};
let suggestions = feedback
.suggestions()
.iter()
.map(|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.",
Suggestion::CapitalizationDoesntHelpVeryMuch => "Nicht nur den ersten Buchstaben groß schreiben.",
Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => "Einige, aber nicht alle Buchstaben groß schreiben.",
Suggestion::ReversedWordsArentMuchHarderToGuess => "Umgekehrte Schreibweise von gebräuchlichen Wörtern vermeiden.",
Suggestion::PredictableSubstitutionsDontHelpVeryMuch => "Vorhersehbare Buchstabenersetzungen wie '@' für 'a' vermeiden.",
Suggestion::UseALongerKeyboardPatternWithMoreTurns => "Längere Tastaturmuster in unterschiedlicher Tipprichtung verwenden.",
Suggestion::AvoidRepeatedWordsAndCharacters => "Wort- und Zeichenwiederholungen vermeiden.",
Suggestion::AvoidSequences => "Häufige Zeichenfolgen vermeiden.",
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.",
}
})
.collect::<Vec<&str>>();
html!(
{
div id="password-strength" class="mb-3 help content is-danger" {
p { ( warning )}
(vorschlag_text) ":"
ul {
@for s in &suggestions {
li { (s) }
}
}
}
}
)
.into_string()
}

View File

@ -1,110 +0,0 @@
use actix_web::HttpResponse;
use maud::html;
use sqlx::PgPool;
use zxcvbn::{zxcvbn, Score};
use crate::utils::{
auth::{generate_salt_and_hash_plain_password, hash_plain_password_with_salt},
ApplicationError,
};
use brass_db::{models::User, Token};
use super::{generate_help_message_for_entropy, PasswordChangeError};
pub struct PasswordChange<'a, T>
where
T: Token,
{
pub(super) pool: &'a PgPool,
pub(super) user_id: i32,
pub(super) password: &'a str,
pub(super) password_retyped: &'a str,
pub(super) token: Option<T>,
pub(super) current_password: Option<&'a str>,
}
impl<T> PasswordChange<'_, T>
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, PasswordChangeError> {
if self.password.chars().count() > 256 {
return Err(PasswordChangeError {
message: html! {
div id="password-strength" class="mb-3 help content is-danger"{
"Password darf nicht länger als 256 Zeichen sein."
}
}
.into_string(),
}
.into());
}
// unwrap is safe, as either a token is present (tokens always require a userid present) or the current password is set (which requires a existing user)
let user = User::read_by_id(self.pool, self.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);
let entropy = zxcvbn(self.password, &user_inputs);
if entropy.score() < Score::Three {
let message = generate_help_message_for_entropy(&entropy);
return Err(PasswordChangeError { message }.into());
}
return Ok(HttpResponse::Ok().body(
html! {
div id="password-strength" class="mb-3 help content is-success" {
@if entropy.score() == Score::Three {
"Sicheres Passwort."
} @else {
"Sehr sicheres Passwort."
}
}
}
.into_string(),
));
}
/// should be called after the form is fully filled and submit is clicked
pub async fn validate(&self) -> Result<(), PasswordChangeError> {
self.validate_for_input().await?;
if let Some(current_password) = self.current_password {
// unwraps are safe, as login only works with password and salt and this call site requires login
let user = User::read_by_id(self.pool, self.user_id).await?.unwrap();
let hash = user.password.as_ref().unwrap();
let salt = user.salt.as_ref().unwrap();
if hash != &hash_plain_password_with_salt(current_password, salt)? {
return Err(PasswordChangeError {
message: "Aktuelles Passwort ist falsch.".to_string(),
}
.into());
}
}
if self.password != self.password_retyped {
return Err(PasswordChangeError {
message: "Passwörter stimmen nicht überein.".to_string(),
}
.into());
}
Ok(())
}
/// commits the password change to the database
pub async fn commit(&self) -> Result<(), ApplicationError> {
let (hash, salt) = generate_salt_and_hash_plain_password(self.password).unwrap();
User::update_password(self.pool, self.user_id, &hash, &salt).await?;
if let Some(token) = &self.token {
token.delete(self.pool).await?;
}
Ok(())
}
}