113 lines
3.8 KiB
Rust
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(())
|
|
}
|
|
}
|