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 hash = hash_plain_password_with_salt(&form.password, &salt).unwrap(); let Some(user) = User::read_for_login(pool.get_ref(), &form.email.to_lowercase()).await? else {
if hash == user.password.unwrap() { return Ok(not_found_response);
Identity::login(&request.extensions(), user.id.to_string()).unwrap(); };
User::update_login_timestamp(pool.get_ref(), user.id) if user.password.is_none() || user.salt.is_none() {
.await return Ok(not_found_response);
.unwrap();
let location = form.next.unwrap_or("/".to_string());
return HttpResponse::Found()
.insert_header(("LOCATION", location.clone()))
.insert_header(("HX-LOCATION", location))
.finish();
}
} }
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())
} }

View File

@ -1,90 +1,89 @@
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) = let reset = PasswordReset::insert_new_for_user(pool.get_ref(), user.id).await?;
User::read_for_login(pool.get_ref(), &form.email.as_ref().unwrap().to_lowercase()).await mailer
{ .send_forgot_password_mail(&user, &reset.token)
let reset = PasswordReset::insert_new_for_user(pool.get_ref(), user.id).await?; .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!")) let Some(token) = PasswordReset::does_token_exist(pool.get_ref(), &form.token).await?
} else if form.email.is_none() else {
&& form.token.is_some() return Ok(HttpResponse::NoContent().finish());
&& 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 token = if let Some(token) = let mut builder = PasswordChangeBuilder::<PasswordReset>::new(
PasswordReset::does_token_exist(pool.get_ref(), form.token.as_ref().unwrap()).await? pool.get_ref(),
{ token.userid,
token &form.password,
} else { &form.passwordretyped,
return Ok(HttpResponse::NoContent().finish());
};
let mut builder = PasswordChangeBuilder::<PasswordReset>::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(),
) )
}; .with_token(token);
return Ok(response); let change = builder.build();
} else {
return Ok(HttpResponse::BadRequest().finish()); 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);
}
} }
} }