diff --git a/db/src/models/availability.rs b/db/src/models/availability.rs index 338b3b29..2b59e247 100644 --- a/db/src/models/availability.rs +++ b/db/src/models/availability.rs @@ -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 .await?; - let adjacent_avaialability = records.first().and_then(|r| { - Some(Availability { - id: r.id, - user_id: r.userid, - user: None, - start: r.starttimestamp.naive_utc(), - end: r.endtimestamp.naive_utc(), - comment: r.comment.clone(), - }) + let adjacent_avaialability = records.first().map(|r| Availability { + id: r.id, + user_id: r.userid, + user: None, + start: r.starttimestamp.naive_utc(), + end: r.endtimestamp.naive_utc(), + comment: r.comment.clone(), }); Ok(adjacent_avaialability) diff --git a/db/src/models/availability_changeset.rs b/db/src/models/availability_changeset.rs index 55b71a5c..90d63aca 100644 --- a/db/src/models/availability_changeset.rs +++ b/db/src/models/availability_changeset.rs @@ -26,17 +26,17 @@ impl<'a> AsyncValidate<'a> for AvailabilityChangeset { &self, context: &'a Self::Context, ) -> Result<(), AsyncValidateError> { - let mut existing_availabilities = - Availability::read_all_by_user_and_date(context.pool, context.user_id, &self.time.0.date()) - .await?; + let mut existing_availabilities = Availability::read_all_by_user_and_date( + context.pool, + context.user_id, + &self.time.0.date(), + ) + .await?; start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?; if let Some(availability) = context.availability { - existing_availabilities = existing_availabilities - .into_iter() - .filter(|a| a.id != availability) - .collect(); + existing_availabilities.retain(|a| a.id != availability); 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( (start, end): &(NaiveDateTime, NaiveDateTime), - existing_availabilities: &Vec, + existing_availabilities: &[Availability], ) -> Result<(), AsyncValidateError> { 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 is_already_present_as_is = existing_availabilities .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 { return Err(AsyncValidateError::new( diff --git a/db/src/models/event_changeset.rs b/db/src/models/event_changeset.rs index 519a066e..155f0086 100644 --- a/db/src/models/event_changeset.rs +++ b/db/src/models/event_changeset.rs @@ -117,7 +117,7 @@ fn date_unchanged_if_edit( async fn time_can_be_extended_if_edit( time: &(NaiveDateTime, NaiveDateTime), event: &Event, - assignments_for_event: &Vec, + assignments_for_event: &[Assignment], pool: &PgPool, ) -> Result<(), AsyncValidateError> { let start = event.start.date(); @@ -197,7 +197,7 @@ async fn time_can_be_extended_if_edit( fn can_unset_fuehrungsassistent( fuehrungsassistent_required: &bool, - assignments_for_event: &Vec, + assignments_for_event: &[Assignment], ) -> Result<(), AsyncValidateError> { if !*fuehrungsassistent_required && assignments_for_event @@ -214,7 +214,7 @@ fn can_unset_fuehrungsassistent( fn can_unset_wachhabender( voluntary_wachhabender: &bool, - assignments_for_event: &Vec, + assignments_for_event: &[Assignment], ) -> Result<(), AsyncValidateError> { if !*voluntary_wachhabender && assignments_for_event diff --git a/db/src/models/export_event_row.rs b/db/src/models/export_event_row.rs index 6a5d581f..176b69c4 100644 --- a/db/src/models/export_event_row.rs +++ b/db/src/models/export_event_row.rs @@ -57,8 +57,8 @@ impl ExportEventRow { voluntary_wachhabender: r.voluntarywachhabender, location_name: r.locationname, event_name: r.eventname, - assignments: r.assignments.unwrap_or(Vec::new()), - vehicles: r.vehicles.unwrap_or(Vec::new()), + assignments: r.assignments.unwrap_or_default(), + vehicles: r.vehicles.unwrap_or_default(), }) .collect(); diff --git a/db/src/models/user.rs b/db/src/models/user.rs index 4153878c..a139f51a 100644 --- a/db/src/models/user.rs +++ b/db/src/models/user.rs @@ -152,7 +152,7 @@ impl User { .fetch_optional(pool) .await?; - Ok(record.and_then(|r| Some(r.id))) + Ok(record.map(|r| r.id)) } pub async fn read_all(pool: &PgPool) -> Result> { diff --git a/db/src/models/user_funtion.rs b/db/src/models/user_funtion.rs index 2283d347..52db4094 100644 --- a/db/src/models/user_funtion.rs +++ b/db/src/models/user_funtion.rs @@ -19,7 +19,7 @@ impl Display for UserFunction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut iterator = self.0.iter().peekable(); while let Some(p) = iterator.next() { - write!(f, "{}", p.to_string())?; + write!(f, "{p}")?; if iterator.peek().is_some() { write!(f, ", ")?; } diff --git a/web/src/endpoints/events/mod.rs b/web/src/endpoints/events/mod.rs index 042816d5..12f24b57 100644 --- a/web/src/endpoints/events/mod.rs +++ b/web/src/endpoints/events/mod.rs @@ -60,13 +60,15 @@ mod short_date_time_format { use chrono::NaiveDateTime; use serde::{self, Deserialize, Deserializer}; + use crate::utils::DateTimeFormat; + pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - const FORMAT: &'static str = "%Y-%m-%dT%H:%M"; + let format = DateTimeFormat::YearMonthDayTHourMinute.into(); 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) } } diff --git a/web/src/endpoints/export/get_availability_data.rs b/web/src/endpoints/export/get_availability_data.rs index ca09c6c6..e6a01422 100644 --- a/web/src/endpoints/export/get_availability_data.rs +++ b/web/src/endpoints/export/get_availability_data.rs @@ -61,8 +61,12 @@ pub async fn get( user.area_id }; - let availabilities = - Availability::read_all_by_daterange_and_area_including_user_for_export(pool.get_ref(), (start_date, end_date), area_id).await?; + let availabilities = Availability::read_all_by_daterange_and_area_including_user_for_export( + pool.get_ref(), + (start_date, end_date), + area_id, + ) + .await?; let export_availabilities = availabilities .into_iter() @@ -73,7 +77,7 @@ pub async fn get( start: a.start, end: a.end, assigned: false, - comment: a.comment.unwrap_or(String::new()), + comment: a.comment.unwrap_or_default(), }) .collect(); diff --git a/web/src/endpoints/export/get_events_data.rs b/web/src/endpoints/export/get_events_data.rs index e0890ae0..eb8d860e 100644 --- a/web/src/endpoints/export/get_events_data.rs +++ b/web/src/endpoints/export/get_events_data.rs @@ -55,8 +55,8 @@ fn read(rows: Vec) -> Vec { hours: (r.end_timestamp - r.start_timestamp).as_seconds_f32() / 3600.0 + 1.0, location: r.location_name.to_string(), name: r.event_name.to_string(), - assigned_name: n.and_then(|s| Some(s.to_string())), - assigned_function: f.and_then(|s| Some(s.to_string())), + assigned_name: n.map(|s| s.to_string()), + assigned_function: f.map(|s| s.to_string()), }; if let Some(assigned_wh) = r @@ -182,11 +182,11 @@ pub async fn get( let buffer = workbook.save_to_buffer().unwrap(); - return Ok(HttpResponse::Ok() + Ok(HttpResponse::Ok() .content_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") .insert_header(( CONTENT_DISPOSITION, ContentDisposition::attachment("export.xlsx"), )) - .body(buffer)); + .body(buffer)) } diff --git a/web/src/endpoints/user/get_reset_password.rs b/web/src/endpoints/user/get_reset_password.rs index 70f7f617..7fb2f64b 100644 --- a/web/src/endpoints/user/get_reset_password.rs +++ b/web/src/endpoints/user/get_reset_password.rs @@ -34,7 +34,10 @@ pub async fn get( } 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 { token, title: "Brass - Passwort zurücksetzen", diff --git a/web/src/endpoints/user/post_new.rs b/web/src/endpoints/user/post_new.rs index b271f6d9..cd1a34f7 100644 --- a/web/src/endpoints/user/post_new.rs +++ b/web/src/endpoints/user/post_new.rs @@ -51,7 +51,10 @@ pub async fn post_new( 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() .body("email: an user already exists with the same email")); } diff --git a/web/src/endpoints/user/post_reset_password.rs b/web/src/endpoints/user/post_reset_password.rs index 0cf280b7..0af1e6dd 100644 --- a/web/src/endpoints/user/post_reset_password.rs +++ b/web/src/endpoints/user/post_reset_password.rs @@ -39,7 +39,7 @@ async fn post( .await?; } - return Ok(HttpResponse::Ok().body("E-Mail versandt!")); + Ok(HttpResponse::Ok().body("E-Mail versandt!")) } Either::Right(form) => { let is_dry = header.is_some_and_equal("password-strength"); @@ -83,7 +83,7 @@ async fn post( ) }; - return Ok(response); + Ok(response) } } } diff --git a/web/src/filters.rs b/web/src/filters.rs index c9aecdd2..56bf8944 100644 --- a/web/src/filters.rs +++ b/web/src/filters.rs @@ -119,12 +119,10 @@ pub fn fmt_time(v: &NaiveTime, format: DateTimeFormat) -> askama::Result } fn escape_html(string: String) -> String { - let s = string + string .replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) - .replace('\'', "'"); - - s + .replace('\'', "'") } diff --git a/web/src/mail/forgot_password.rs b/web/src/mail/forgot_password.rs index 84abbd89..a8db9547 100644 --- a/web/src/mail/forgot_password.rs +++ b/web/src/mail/forgot_password.rs @@ -58,7 +58,7 @@ fn build( let sender_mailbox = Mailbox::new( Some("noreply".to_string()), - Address::new("noreply", &hostname)?, + Address::new("noreply", hostname)?, ); let message = Message::builder() diff --git a/web/src/mail/registration.rs b/web/src/mail/registration.rs index 28e66327..55affb95 100644 --- a/web/src/mail/registration.rs +++ b/web/src/mail/registration.rs @@ -62,7 +62,7 @@ fn build( let sender_mailbox = Mailbox::new( Some("noreply".to_string()), - Address::new("noreply", &hostname)?, + Address::new("noreply", hostname)?, ); let message = Message::builder() diff --git a/web/src/utils/application_error.rs b/web/src/utils/application_error.rs index fd3100c1..8cb5d1f9 100644 --- a/web/src/utils/application_error.rs +++ b/web/src/utils/application_error.rs @@ -28,8 +28,8 @@ pub enum ApplicationError { impl actix_web::error::ResponseError for ApplicationError { fn status_code(&self) -> StatusCode { match *self { - ApplicationError::UnsupportedEnumValue { .. } => StatusCode::BAD_REQUEST, - ApplicationError::Unauthorized { .. } => StatusCode::UNAUTHORIZED, + ApplicationError::UnsupportedEnumValue(_) => StatusCode::BAD_REQUEST, + ApplicationError::Unauthorized => StatusCode::UNAUTHORIZED, ApplicationError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, ApplicationError::EnvVariable(_) => StatusCode::INTERNAL_SERVER_ERROR, ApplicationError::EmailAdress(_) => StatusCode::INTERNAL_SERVER_ERROR, diff --git a/web/src/utils/htmx_target_header.rs b/web/src/utils/htmx_target_header.rs index 199a32a0..427f8927 100644 --- a/web/src/utils/htmx_target_header.rs +++ b/web/src/utils/htmx_target_header.rs @@ -20,7 +20,7 @@ impl Header for HtmxTargetHeader { return Err(ParseError::Header); } - return Ok(self::HtmxTargetHeader(Some(line.to_string()))); + Ok(self::HtmxTargetHeader(Some(line.to_string()))) } else { Ok(self::HtmxTargetHeader(None)) } @@ -33,7 +33,7 @@ impl TryIntoHeaderValue for HtmxTargetHeader { #[inline] fn try_into_value(self) -> Result { if let Some(s) = self.0 { - return s.try_into_value(); + s.try_into_value() } else { HeaderValue::from_str("") } diff --git a/web/src/utils/password_change/password_change_builder.rs b/web/src/utils/password_change/builder.rs similarity index 100% rename from web/src/utils/password_change/password_change_builder.rs rename to web/src/utils/password_change/builder.rs diff --git a/web/src/utils/password_change/command.rs b/web/src/utils/password_change/command.rs new file mode 100644 index 00000000..451002a6 --- /dev/null +++ b/web/src/utils/password_change/command.rs @@ -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, + pub(super) current_password: Option<&'a str>, +} + +impl 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 { + 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::>(); + + 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() +} diff --git a/web/src/utils/password_change/error.rs b/web/src/utils/password_change/error.rs new file mode 100644 index 00000000..a01053cb --- /dev/null +++ b/web/src/utils/password_change/error.rs @@ -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 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!") + } +} diff --git a/web/src/utils/password_change/mod.rs b/web/src/utils/password_change/mod.rs index 9428913b..c44cf811 100644 --- a/web/src/utils/password_change/mod.rs +++ b/web/src/utils/password_change/mod.rs @@ -1,126 +1,7 @@ -use maud::html; -use thiserror::Error; -use tracing::error; -use zxcvbn::{ - feedback::{Suggestion, Warning}, - Entropy, -}; +mod builder; +mod command; +mod error; -mod password_change; -mod password_change_builder; - -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 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(); - - 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::>(); - - 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() -} +pub use builder::PasswordChangeBuilder; +pub use command::PasswordChange; +pub use error::PasswordChangeError; diff --git a/web/src/utils/password_change/password_change.rs b/web/src/utils/password_change/password_change.rs deleted file mode 100644 index 1cc3d046..00000000 --- a/web/src/utils/password_change/password_change.rs +++ /dev/null @@ -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, - pub(super) current_password: Option<&'a str>, -} - -impl 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 { - 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(()) - } -}