From ee4481225e77bced49c7aa92cec95534e8e0edf6 Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Sun, 13 Jul 2025 11:52:33 +0200 Subject: [PATCH] refactor: reset password --- db/src/models/user.rs | 32 ++--- web/src/endpoints/user/get_reset_password.rs | 4 +- web/src/endpoints/user/post_login.rs | 45 +++--- web/src/endpoints/user/post_reset_password.rs | 129 +++++++++--------- 4 files changed, 109 insertions(+), 101 deletions(-) diff --git a/db/src/models/user.rs b/db/src/models/user.rs index 97d069f4..4153878c 100644 --- a/db/src/models/user.rs +++ b/db/src/models/user.rs @@ -107,7 +107,7 @@ impl User { Ok(user) } - pub async fn read_for_login(pool: &PgPool, email: &str) -> Result { + pub async fn read_for_login(pool: &PgPool, email: &str) -> Result> { let record = sqlx::query!( r#" SELECT id, @@ -126,25 +126,25 @@ impl User { "#, email, ) - .fetch_one(pool) + .fetch_optional(pool) .await?; - let result = User { - id: record.id, - name: record.name, - email: record.email, - password: record.password, - salt: record.salt, - role: record.role, - function: record.function, - area_id: record.areaid, + let user = record.map(|r| User { + id: r.id, + name: r.name, + email: r.email, + password: r.password, + salt: r.salt, + role: r.role, + function: r.function, + area_id: r.areaid, area: None, - locked: record.locked, - last_login: record.lastlogin, - receive_notifications: record.receivenotifications, - }; + locked: r.locked, + last_login: r.lastlogin, + receive_notifications: r.receivenotifications, + }); - Ok(result) + Ok(user) } pub async fn exists(pool: &PgPool, email: &str) -> Result> { diff --git a/web/src/endpoints/user/get_reset_password.rs b/web/src/endpoints/user/get_reset_password.rs index 64bf46d5..70f7f617 100644 --- a/web/src/endpoints/user/get_reset_password.rs +++ b/web/src/endpoints/user/get_reset_password.rs @@ -31,7 +31,9 @@ pub async fn get( return Ok(HttpResponse::Found() .insert_header((LOCATION, "/")) .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? { let template = ResetPasswordTemplate { token, diff --git a/web/src/endpoints/user/post_login.rs b/web/src/endpoints/user/post_login.rs index d10c11bc..04f26bba 100644 --- a/web/src/endpoints/user/post_login.rs +++ b/web/src/endpoints/user/post_login.rs @@ -4,7 +4,7 @@ use brass_db::models::User; use serde::{Deserialize, Serialize}; 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)] pub struct LoginForm { @@ -18,26 +18,33 @@ async fn post( web::Form(form): web::Form, request: HttpRequest, pool: web::Data, -) -> impl Responder { - if let Ok(user) = User::read_for_login(pool.get_ref(), &form.email.to_lowercase()).await { - let salt = user.salt.unwrap(); +) -> Result { + let not_found_response = HttpResponse::BadRequest().body("E-Mail oder Passwort falsch."); - 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(); + let Some(user) = User::read_for_login(pool.get_ref(), &form.email.to_lowercase()).await? else { + return Ok(not_found_response); + }; - User::update_login_timestamp(pool.get_ref(), user.id) - .await - .unwrap(); - - let location = form.next.unwrap_or("/".to_string()); - - return HttpResponse::Found() - .insert_header(("LOCATION", location.clone())) - .insert_header(("HX-LOCATION", location)) - .finish(); - } + if user.password.is_none() || user.salt.is_none() { + return Ok(not_found_response); } - HttpResponse::BadRequest().body("E-Mail oder Passwort falsch.") + 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); + } + + Identity::login(&request.extensions(), user.id.to_string()).unwrap(); + + User::update_login_timestamp(pool.get_ref(), user.id).await?; + + let location = form.next.unwrap_or("/".to_string()); + + Ok(HttpResponse::Found() + .insert_header(("LOCATION", location.clone())) + .insert_header(("HX-LOCATION", location)) + .finish()) } diff --git a/web/src/endpoints/user/post_reset_password.rs b/web/src/endpoints/user/post_reset_password.rs index 0f91c246..0cf280b7 100644 --- a/web/src/endpoints/user/post_reset_password.rs +++ b/web/src/endpoints/user/post_reset_password.rs @@ -1,90 +1,89 @@ -use actix_web::{web, HttpResponse, Responder}; +use actix_web::{web, Either, HttpResponse, Responder}; use maud::html; use serde::Deserialize; use sqlx::PgPool; use crate::{ mail::Mailer, - utils::{password_change::PasswordChangeBuilder, ApplicationError}, + utils::{password_change::PasswordChangeBuilder, ApplicationError, HtmxTargetHeader}, }; use brass_db::models::{PasswordReset, User}; +#[derive(Deserialize, Debug)] +struct RequestResetPasswordForm { + email: String, +} + #[derive(Deserialize, Debug)] struct ResetPasswordForm { - email: Option, - token: Option, - password: Option, - passwordretyped: Option, - dry: Option, + token: String, + password: String, + passwordretyped: String, } #[actix_web::post("/reset-password")] async fn post( - form: web::Form, + form: Either, web::Form>, + header: web::Header, pool: web::Data, mailer: web::Data, ) -> Result { - if form.email.is_some() - && form.token.is_none() - && form.password.is_none() - && form.passwordretyped.is_none() - { - 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?; - mailer - .send_forgot_password_mail(&user, &reset.token) - .await?; + match form { + Either::Left(form) => { + if let Some(user) = + User::read_for_login(pool.get_ref(), &form.email.to_lowercase()).await? + { + let reset = PasswordReset::insert_new_for_user(pool.get_ref(), user.id).await?; + mailer + .send_forgot_password_mail(&user, &reset.token) + .await?; + } + + return Ok(HttpResponse::Ok().body("E-Mail versandt!")); } + Either::Right(form) => { + let is_dry = header.is_some_and_equal("password-strength"); - Ok(HttpResponse::Ok().body("E-Mail versandt!")) - } else if form.email.is_none() - && form.token.is_some() - && form.password.is_some() - && 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 Some(token) = PasswordReset::does_token_exist(pool.get_ref(), &form.token).await? + else { + return Ok(HttpResponse::NoContent().finish()); + }; - let token = if let Some(token) = - PasswordReset::does_token_exist(pool.get_ref(), form.token.as_ref().unwrap()).await? - { - token - } else { - return Ok(HttpResponse::NoContent().finish()); - }; - - let mut builder = PasswordChangeBuilder::::new( - pool.get_ref(), - token.userid, - &form.password.as_ref().unwrap(), - &form.passwordretyped.as_ref().unwrap(), - ) - .with_token(token); - - let change = builder.build(); - - let response = if is_dry { - change.validate_for_input().await.unwrap() // TODO: - } else { - change.validate().await.unwrap(); // TODO - change.commit().await?; - HttpResponse::Ok().body( - html! { - div class="block mb-3" { - "Passwort erfolgreich geändert." - } - a class="block button is-primary" hx-boost="true" href="/login"{ - "Zum Login" - } - } - .into_string(), + let mut builder = PasswordChangeBuilder::::new( + pool.get_ref(), + token.userid, + &form.password, + &form.passwordretyped, ) - }; + .with_token(token); - return Ok(response); - } else { - return Ok(HttpResponse::BadRequest().finish()); + let change = builder.build(); + + let response = if is_dry { + match change.validate_for_input().await { + Ok(r) => r, + Err(e) => HttpResponse::UnprocessableEntity().body(e.message), + } + } else { + if let Err(e) = change.validate().await { + return Ok(HttpResponse::UnprocessableEntity().body(e.message)); + } + + change.commit().await?; + HttpResponse::Ok().body( + html! { + div class="block mb-3" { + "Passwort erfolgreich geändert." + } + a class="block button is-primary" hx-boost="true" href="/login"{ + "Zum Login" + } + } + .into_string(), + ) + }; + + return Ok(response); + } } }