brass/web/src/utils/password_change/password_change.rs

113 lines
3.8 KiB
Rust

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<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, ApplicationError> {
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(())
}
}