Sqlx(#[from] sqlx::Error),
#[error(
- "Unsupported database version: {0} (code version: {})",
- CURRENT_DB_VERSION
+ "Unsupported database version: {0} (application version: {current})",
+ current = CURRENT_DB_VERSION
)]
UnsupportedVersion(u32),
#[derive(Debug)]
pub enum GetTokenResetPassword {
PasswordAlreadyReset,
+ EmailUnknown,
Ok(String),
}
) -> Result<GetTokenResetPassword> {
let mut tx = self.tx().await?;
- if let Some(db_datetime) = sqlx::query_scalar::<_, Option<DateTime<Utc>>>(
+ if let Some(db_datetime_nullable) = sqlx::query_scalar::<_, Option<DateTime<Utc>>>(
r#"
SELECT [password_reset_datetime]
FROM [User]
"#,
)
.bind(email)
- .fetch_one(&mut *tx)
+ .fetch_optional(&mut *tx)
.await?
{
- if Utc::now() - db_datetime <= validation_time {
- return Ok(GetTokenResetPassword::PasswordAlreadyReset);
+ if let Some(db_datetime) = db_datetime_nullable {
+ if Utc::now() - db_datetime <= validation_time {
+ return Ok(GetTokenResetPassword::PasswordAlreadyReset);
+ }
}
+ } else {
+ return Ok(GetTokenResetPassword::EmailUnknown);
}
let token = generate_token();
Ok(())
}
+ #[tokio::test]
+ async fn ask_to_reset_password_for_unknown_email() -> Result<()> {
+ let connection = Connection::new_in_memory().await?;
+
+ let email = "paul@atreides.com";
+
+ // Ask for password reset.
+ match connection
+ .get_token_reset_password(email, Duration::hours(1))
+ .await?
+ {
+ GetTokenResetPassword::EmailUnknown => Ok(()), // Nominal case.
+ other => panic!("{:?}", other),
+ }
+ }
+
#[tokio::test]
async fn sign_up_then_send_validation_then_sign_out_then_ask_to_reset_password() -> Result<()> {
let connection = Connection::new_in_memory().await?;
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,
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;
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,
}
}
-#[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> {
+ 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_get(
Extension(user): Extension<Option<model::User>>,
+ Query(query): Query<HashMap<String, String>>,
) -> Result<Response> {
- Ok("todo".into_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() -> Result<Response> {
- Ok("todo".into_response())
+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 /////