use actix_web::HttpResponse; use maud::html; use sqlx::PgPool; use zxcvbn::{zxcvbn, Score}; use crate::{ models::{Token, User}, utils::{ auth::{generate_salt_and_hash_plain_password, hash_plain_password_with_salt}, ApplicationError, }, }; 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<(), ApplicationError> { 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(()) } }