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)
}
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!(
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<Option<i32>> {

View File

@ -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,

View File

@ -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<LoginForm>,
request: HttpRequest,
pool: web::Data<PgPool>,
) -> impl Responder {
if let Ok(user) = User::read_for_login(pool.get_ref(), &form.email.to_lowercase()).await {
let salt = user.salt.unwrap();
) -> Result<impl Responder, ApplicationError> {
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())
}

View File

@ -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<String>,
token: Option<String>,
password: Option<String>,
passwordretyped: Option<String>,
dry: Option<bool>,
token: String,
password: String,
passwordretyped: String,
}
#[actix_web::post("/reset-password")]
async fn post(
form: web::Form<ResetPasswordForm>,
form: Either<web::Form<RequestResetPasswordForm>, web::Form<ResetPasswordForm>>,
header: web::Header<HtmxTargetHeader>,
pool: web::Data<PgPool>,
mailer: web::Data<Mailer>,
) -> Result<impl Responder, ApplicationError> {
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::<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(),
let mut builder = PasswordChangeBuilder::<PasswordReset>::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);
}
}
}