refactor: reset password

This commit is contained in:
Max Hohlfeld 2025-07-13 11:52:33 +02:00
parent ab648dd4f2
commit ee4481225e
4 changed files with 109 additions and 101 deletions

View File

@ -107,7 +107,7 @@ impl User {
Ok(user) Ok(user)
} }
pub async fn read_for_login(pool: &PgPool, email: &str) -> Result<User> { pub async fn read_for_login(pool: &PgPool, email: &str) -> Result<Option<User>> {
let record = sqlx::query!( let record = sqlx::query!(
r#" r#"
SELECT id, SELECT id,
@ -126,25 +126,25 @@ impl User {
"#, "#,
email, email,
) )
.fetch_one(pool) .fetch_optional(pool)
.await?; .await?;
let result = User { let user = record.map(|r| User {
id: record.id, id: r.id,
name: record.name, name: r.name,
email: record.email, email: r.email,
password: record.password, password: r.password,
salt: record.salt, salt: r.salt,
role: record.role, role: r.role,
function: record.function, function: r.function,
area_id: record.areaid, area_id: r.areaid,
area: None, area: None,
locked: record.locked, locked: r.locked,
last_login: record.lastlogin, last_login: r.lastlogin,
receive_notifications: record.receivenotifications, receive_notifications: r.receivenotifications,
}; });
Ok(result) Ok(user)
} }
pub async fn exists(pool: &PgPool, email: &str) -> Result<Option<i32>> { pub async fn exists(pool: &PgPool, email: &str) -> Result<Option<i32>> {

View File

@ -31,7 +31,9 @@ pub async fn get(
return Ok(HttpResponse::Found() return Ok(HttpResponse::Found()
.insert_header((LOCATION, "/")) .insert_header((LOCATION, "/"))
.finish()); .finish());
} else 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 let Some(_) = PasswordReset::does_token_exist(pool.get_ref(), token).await? {
let template = ResetPasswordTemplate { let template = ResetPasswordTemplate {
token, token,

View File

@ -4,7 +4,7 @@ use brass_db::models::User;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
use crate::utils::auth::hash_plain_password_with_salt; use crate::utils::{auth::hash_plain_password_with_salt, ApplicationError};
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct LoginForm { pub struct LoginForm {
@ -18,26 +18,33 @@ async fn post(
web::Form(form): web::Form<LoginForm>, web::Form(form): web::Form<LoginForm>,
request: HttpRequest, request: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> impl Responder { ) -> Result<impl Responder, ApplicationError> {
if let Ok(user) = User::read_for_login(pool.get_ref(), &form.email.to_lowercase()).await { let not_found_response = HttpResponse::BadRequest().body("E-Mail oder Passwort falsch.");
let salt = user.salt.unwrap();
let Some(user) = User::read_for_login(pool.get_ref(), &form.email.to_lowercase()).await? else {
return Ok(not_found_response);
};
if user.password.is_none() || user.salt.is_none() {
return Ok(not_found_response);
}
let password = user.password.unwrap();
let salt = user.salt.unwrap();
let hash = hash_plain_password_with_salt(&form.password, &salt)?;
if hash != password {
return Ok(not_found_response);
}
let hash = hash_plain_password_with_salt(&form.password, &salt).unwrap();
if hash == user.password.unwrap() {
Identity::login(&request.extensions(), user.id.to_string()).unwrap(); Identity::login(&request.extensions(), user.id.to_string()).unwrap();
User::update_login_timestamp(pool.get_ref(), user.id) User::update_login_timestamp(pool.get_ref(), user.id).await?;
.await
.unwrap();
let location = form.next.unwrap_or("/".to_string()); let location = form.next.unwrap_or("/".to_string());
return HttpResponse::Found() Ok(HttpResponse::Found()
.insert_header(("LOCATION", location.clone())) .insert_header(("LOCATION", location.clone()))
.insert_header(("HX-LOCATION", location)) .insert_header(("HX-LOCATION", location))
.finish(); .finish())
}
}
HttpResponse::BadRequest().body("E-Mail oder Passwort falsch.")
} }

View File

@ -1,36 +1,37 @@
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, Either, HttpResponse, Responder};
use maud::html; use maud::html;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use crate::{ use crate::{
mail::Mailer, mail::Mailer,
utils::{password_change::PasswordChangeBuilder, ApplicationError}, utils::{password_change::PasswordChangeBuilder, ApplicationError, HtmxTargetHeader},
}; };
use brass_db::models::{PasswordReset, User}; use brass_db::models::{PasswordReset, User};
#[derive(Deserialize, Debug)]
struct RequestResetPasswordForm {
email: String,
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct ResetPasswordForm { struct ResetPasswordForm {
email: Option<String>, token: String,
token: Option<String>, password: String,
password: Option<String>, passwordretyped: String,
passwordretyped: Option<String>,
dry: Option<bool>,
} }
#[actix_web::post("/reset-password")] #[actix_web::post("/reset-password")]
async fn post( async fn post(
form: web::Form<ResetPasswordForm>, form: Either<web::Form<RequestResetPasswordForm>, web::Form<ResetPasswordForm>>,
header: web::Header<HtmxTargetHeader>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
mailer: web::Data<Mailer>, mailer: web::Data<Mailer>,
) -> Result<impl Responder, ApplicationError> { ) -> Result<impl Responder, ApplicationError> {
if form.email.is_some() match form {
&& form.token.is_none() Either::Left(form) => {
&& form.password.is_none() if let Some(user) =
&& form.passwordretyped.is_none() User::read_for_login(pool.get_ref(), &form.email.to_lowercase()).await?
{
if let Ok(user) =
User::read_for_login(pool.get_ref(), &form.email.as_ref().unwrap().to_lowercase()).await
{ {
let reset = PasswordReset::insert_new_for_user(pool.get_ref(), user.id).await?; let reset = PasswordReset::insert_new_for_user(pool.get_ref(), user.id).await?;
mailer mailer
@ -38,37 +39,36 @@ async fn post(
.await?; .await?;
} }
Ok(HttpResponse::Ok().body("E-Mail versandt!")) return Ok(HttpResponse::Ok().body("E-Mail versandt!"));
} else if form.email.is_none() }
&& form.token.is_some() Either::Right(form) => {
&& form.password.is_some() let is_dry = header.is_some_and_equal("password-strength");
&& form.passwordretyped.is_some()
{
// TODO: refactor into check if HX-TARGET = #password-strength exists
let is_dry = form.dry.is_some_and(|b| b);
let token = if let Some(token) = let Some(token) = PasswordReset::does_token_exist(pool.get_ref(), &form.token).await?
PasswordReset::does_token_exist(pool.get_ref(), form.token.as_ref().unwrap()).await? else {
{
token
} else {
return Ok(HttpResponse::NoContent().finish()); return Ok(HttpResponse::NoContent().finish());
}; };
let mut builder = PasswordChangeBuilder::<PasswordReset>::new( let mut builder = PasswordChangeBuilder::<PasswordReset>::new(
pool.get_ref(), pool.get_ref(),
token.userid, token.userid,
&form.password.as_ref().unwrap(), &form.password,
&form.passwordretyped.as_ref().unwrap(), &form.passwordretyped,
) )
.with_token(token); .with_token(token);
let change = builder.build(); let change = builder.build();
let response = if is_dry { let response = if is_dry {
change.validate_for_input().await.unwrap() // TODO: match change.validate_for_input().await {
Ok(r) => r,
Err(e) => HttpResponse::UnprocessableEntity().body(e.message),
}
} else { } else {
change.validate().await.unwrap(); // TODO if let Err(e) = change.validate().await {
return Ok(HttpResponse::UnprocessableEntity().body(e.message));
}
change.commit().await?; change.commit().await?;
HttpResponse::Ok().body( HttpResponse::Ok().body(
html! { html! {
@ -84,7 +84,6 @@ async fn post(
}; };
return Ok(response); return Ok(response);
} else { }
return Ok(HttpResponse::BadRequest().finish());
} }
} }