Add a way to reset password

This commit is contained in:
Greg Burri 2024-11-09 11:22:53 +01:00
parent 5d343c273f
commit ed979719b5
12 changed files with 352 additions and 57 deletions

View file

@ -4,7 +4,7 @@ use askama::Template;
use axum::{
body::Body,
debug_handler,
extract::{ConnectInfo, Extension, Host, Path, Query, Request, State},
extract::{connect_info, ConnectInfo, Extension, Host, Path, Query, Request, State},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Redirect, Response, Result},
Form,
@ -12,8 +12,14 @@ use axum::{
use axum_extra::extract::cookie::{Cookie, CookieJar};
use chrono::Duration;
use serde::Deserialize;
use tracing::{event, Level};
use crate::{config::Config, consts, data::db, email, model, utils, AppState};
use crate::{
config::Config,
consts::{self, VALIDATION_PASSWORD_RESET_TOKEN_DURATION},
data::db,
email, model, utils, AppState,
};
pub mod webapi;
@ -284,32 +290,15 @@ pub async fn sign_up_post(
error_response(SignUpError::UserAlreadyExists, &form_data, user)
}
Ok(db::SignUpResult::UserCreatedWaitingForValidation(token)) => {
let url = {
let port: Option<u16> = 'p: {
let split_port: Vec<&str> = host.split(':').collect();
if split_port.len() == 2 {
if let Ok(p) = split_port[1].parse::<u16>() {
break 'p Some(p);
}
}
None
};
format!(
"http{}://{}",
if port.is_some() && port.unwrap() != 443 {
""
} else {
"s"
},
host
)
};
let url = utils::get_url_from_host(&host);
let email = form_data.email.clone();
match email::send_validation(
&url,
match email::send_email(
&email,
&token,
&format!(
"Follow this link to confirm your inscription: {}/validation?validation_token={}",
url, token
),
&config.smtp_relay_address,
&config.smtp_login,
&config.smtp_password,
@ -523,16 +512,218 @@ pub async fn ask_reset_password_get(
}
}
#[debug_handler]
#[derive(Deserialize, Debug)]
pub struct AskResetPasswordForm {
email: String,
}
enum AskResetPasswordError {
InvalidEmail,
EmailAlreadyReset,
EmailUnknown,
UnableSendEmail,
DatabaseError,
}
#[debug_handler(state = AppState)]
pub async fn ask_reset_password_post(
Host(host): Host,
State(connection): State<db::Connection>,
State(config): State<Config>,
Extension(user): Extension<Option<model::User>>,
Form(form_data): Form<AskResetPasswordForm>,
) -> Result<Response> {
Ok("todo".into_response())
fn error_response(
error: AskResetPasswordError,
email: &str,
user: Option<model::User>,
) -> Result<Response> {
Ok(AskResetPasswordTemplate {
user,
email: email.to_string(),
message_email: match error {
AskResetPasswordError::InvalidEmail => "Invalid email",
_ => "",
}
.to_string(),
message: match error {
AskResetPasswordError::EmailAlreadyReset => {
"The password has already been reset for this email"
}
AskResetPasswordError::EmailUnknown => "Email unknown",
AskResetPasswordError::UnableSendEmail => "Unable to send the reset password email",
AskResetPasswordError::DatabaseError => "Database error",
_ => "",
}
.to_string(),
}
.into_response())
}
// Validation of email.
if let common::utils::EmailValidation::NotValid =
common::utils::validate_email(&form_data.email)
{
return error_response(AskResetPasswordError::InvalidEmail, &form_data.email, user);
}
match connection
.get_token_reset_password(
&form_data.email,
Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION),
)
.await
{
Ok(db::GetTokenResetPassword::PasswordAlreadyReset) => error_response(
AskResetPasswordError::EmailAlreadyReset,
&form_data.email,
user,
),
Ok(db::GetTokenResetPassword::EmailUnknown) => {
error_response(AskResetPasswordError::EmailUnknown, &form_data.email, user)
}
Ok(db::GetTokenResetPassword::Ok(token)) => {
let url = utils::get_url_from_host(&host);
match email::send_email(
&form_data.email,
&format!(
"Follow this link to reset your password: {}/reset_password?reset_token={}",
url, token
),
&config.smtp_relay_address,
&config.smtp_login,
&config.smtp_password,
)
.await
{
Ok(()) => Ok(MessageTemplate {
user,
message: "An email has been sent, follow the link to reset your password.",
}
.into_response()),
Err(_) => {
// error!("Email validation error: {}", error); // TODO: log
error_response(
AskResetPasswordError::UnableSendEmail,
&form_data.email,
user,
)
}
}
}
Err(error) => {
event!(Level::ERROR, "{}", error);
error_response(AskResetPasswordError::DatabaseError, &form_data.email, user)
}
}
}
#[derive(Template)]
#[template(path = "reset_password.html")]
struct ResetPasswordTemplate {
user: Option<model::User>,
reset_token: String,
password_1: String,
password_2: String,
message: String,
message_password: String,
}
#[debug_handler]
pub async fn reset_password() -> Result<Response> {
Ok("todo".into_response())
pub async fn reset_password_get(
Extension(user): Extension<Option<model::User>>,
Query(query): Query<HashMap<String, String>>,
) -> Result<Response> {
if let Some(reset_token) = query.get("reset_token") {
Ok(ResetPasswordTemplate {
user,
reset_token: reset_token.to_string(),
password_1: String::new(),
password_2: String::new(),
message: String::new(),
message_password: String::new(),
}
.into_response())
} else {
Ok(MessageTemplate {
user,
message: "Reset token missing",
}
.into_response())
}
}
#[derive(Deserialize, Debug)]
pub struct ResetPasswordForm {
password_1: String,
password_2: String,
reset_token: String,
}
enum ResetPasswordError {
PasswordsNotEqual,
InvalidPassword,
DatabaseError,
}
#[debug_handler]
pub async fn reset_password_post(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Form(form_data): Form<ResetPasswordForm>,
) -> Result<Response> {
fn error_response(
error: ResetPasswordError,
form_data: &ResetPasswordForm,
user: Option<model::User>,
) -> Result<Response> {
Ok(ResetPasswordTemplate {
user,
reset_token: form_data.reset_token.clone(),
password_1: String::new(),
password_2: String::new(),
message_password: match error {
ResetPasswordError::PasswordsNotEqual => "Passwords don't match",
ResetPasswordError::InvalidPassword => {
"Password must have at least eight characters"
}
_ => "",
}
.to_string(),
message: match error {
ResetPasswordError::DatabaseError => "Database error",
_ => "",
}
.to_string(),
}
.into_response())
}
if form_data.password_1 != form_data.password_2 {
return error_response(ResetPasswordError::PasswordsNotEqual, &form_data, user);
}
if let common::utils::PasswordValidation::TooShort =
common::utils::validate_password(&form_data.password_1)
{
return error_response(ResetPasswordError::InvalidPassword, &form_data, user);
}
match connection
.reset_password(
&form_data.password_1,
&form_data.reset_token,
Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION),
)
.await
{
Ok(_) => Ok(MessageTemplate {
user,
message: "Your password has been reset",
}
.into_response()),
Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, user),
}
}
///// 404 /////