Do not update user email if there is an error when sending the validation link to the new email

This commit is contained in:
Greg Burri 2025-05-07 01:12:51 +02:00
parent 45f4e2d169
commit 3ab168fd67
10 changed files with 103 additions and 68 deletions

12
Cargo.lock generated
View file

@ -2586,9 +2586,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.26"
version = "0.23.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
dependencies = [
"log",
"once_cell",
@ -2607,9 +2607,9 @@ checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
[[package]]
name = "rustls-webpki"
version = "0.103.1"
version = "0.103.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
checksum = "7149975849f1abb3832b246010ef62ccc80d3a76169517ada7188252b9cfb437"
dependencies = [
"ring",
"rustls-pki-types",
@ -3368,9 +3368,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.44.2"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",

View file

@ -5,15 +5,19 @@ $dark-theme: false !default;
$color-1: #B29B89;
$color-2: #89B29B;
$color-3: #9B89B2;
$color-highlight: #cf2d2dff;
$text-color: color.adjust($color-1, $lightness: -30%);
$text-highlight: color.adjust($color-1, $lightness: +30%);
$link-color: color.adjust($color-3, $lightness: -25%);
$link-hover-color: color.adjust($color-3, $lightness: +20%);
@if $dark-theme {
$text-color: color.adjust($color-1, $lightness: -10%);
$text-highlight: color.adjust($color-1, $lightness: +10%);
$color-highlight: color.adjust($color-highlight, $lightness: +10%);
$link-color: color.adjust($color-3, $lightness: -5%);
$link-hover-color: color.adjust($color-3, $lightness: +10%);

View file

@ -28,6 +28,7 @@ body {
.user-message {
font-weight: bold;
color: consts.$color-highlight;
}
.drag-handle {
@ -286,7 +287,7 @@ body {
}
}
#user-edit form {
#reset-password form {
grid-template-columns: auto 1fr auto;
input[type="submit"] {
@ -294,48 +295,13 @@ body {
}
}
// #sign-in {
#user-edit form {
grid-template-columns: auto 1fr auto;
// }
// #user-edit {
// .label-name {
// grid-column: 1;
// grid-row: 1;
// }
// .input-name {
// grid-column: 2;
// grid-row: 1;
// }
// .label-password-1 {
// grid-column: 1;
// grid-row: 2;
// }
// .input-password-1 {
// grid-column: 2;
// grid-row: 2;
// }
// .label-password-2 {
// grid-column: 1;
// grid-row: 3;
// }
// .input-password-2 {
// grid-column: 2;
// grid-row: 3;
// }
// .button-save {
// grid-column: 2;
// grid-row: 4;
// width: fit-content;
// justify-self: flex-end;
// }
//}
input[type="submit"] {
grid-column: 2
}
}
}
}

View file

@ -8,6 +8,7 @@ use crate::{
consts,
data::model,
hash::{hash, verify_password},
services::user,
};
#[derive(Debug, Display)]
@ -20,8 +21,8 @@ pub enum SignUpResult {
#[derive(Debug, Display)]
pub enum UpdateUserResult {
EmailAlreadyTaken,
/// Validation token.
UserUpdatedWaitingForRevalidation(String),
/// (New validation token, old email, old validation token, old validation time).
UserUpdatedWaitingForRevalidation(String, String, Option<String>, String),
Ok,
}
@ -105,11 +106,22 @@ FROM [UserLoginToken] WHERE [token] = $1
let mut tx = self.tx().await?;
let hashed_new_password = new_password.map(|p| hash(p).unwrap());
let (email, name, default_servings, first_day_of_the_week, hashed_password) = sqlx::query_as::<
_,
(String, String, u32, u8, String),
>(
"SELECT [email], [name], [default_servings], [first_day_of_the_week], [password] FROM [User] WHERE [id] = $1",
let (
email,
name,
default_servings,
first_day_of_the_week,
hashed_password,
validation_token,
validation_token_datetime,
) = sqlx::query_as::<_, (String, String, u32, u8, String, Option<String>, String)>(
r#"
SELECT
[email], [name], [default_servings],
[first_day_of_the_week], [password],
[validation_token], [validation_token_datetime]
FROM [User] WHERE [id] = $1
"#,
)
.bind(user_id)
.fetch_one(&mut *tx)
@ -119,7 +131,7 @@ FROM [UserLoginToken] WHERE [token] = $1
let email_changed = new_email.is_some_and(|new_email| new_email != email);
// Check if email not already taken.
let validation_token = if email_changed {
let new_validation_token = if email_changed {
if sqlx::query_scalar(
r#"
SELECT COUNT(*) > 0
@ -170,13 +182,41 @@ WHERE [id] = $1
tx.commit().await?;
Ok(if let Some(validation_token) = validation_token {
UpdateUserResult::UserUpdatedWaitingForRevalidation(validation_token)
Ok(if let Some(new_validation_token) = new_validation_token {
UpdateUserResult::UserUpdatedWaitingForRevalidation(
new_validation_token,
email,
validation_token,
validation_token_datetime,
)
} else {
UpdateUserResult::Ok
})
}
pub async fn update_user_email_and_token(
&self,
user_id: i64,
email: &str,
validation_token: Option<&str>,
validation_token_datetime: &str,
) -> Result<()> {
sqlx::query(
r#"
UPDATE [User]
SET [email] = $2, [validation_token] = $3, [validation_token_datetime] = $4
WHERE [id] = $1"#,
)
.bind(user_id)
.bind(email)
.bind(validation_token)
.bind(validation_token_datetime)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
pub async fn set_user_lang(&self, user_id: i64, lang: &str) -> Result<()> {
sqlx::query("UPDATE [User] SET [lang] = $2 WHERE [id] = $1")
.bind(user_id)
@ -975,7 +1015,7 @@ VALUES
assert_eq!(user.name, "paul");
assert_eq!(user.email, "paul@atreides.com");
if let UpdateUserResult::UserUpdatedWaitingForRevalidation(token) = connection
if let UpdateUserResult::UserUpdatedWaitingForRevalidation(token, _, _, _) = connection
.update_user(
1,
Some("muaddib@fremen.com"),

View file

@ -474,9 +474,6 @@ pub async fn ask_reset_password_post(
email,
message_email: match error {
AskResetPasswordError::InvalidEmail => context.tr.t(Sentence::InvalidEmail),
_ => "",
},
message: match error {
AskResetPasswordError::EmailAlreadyReset => {
context.tr.t(Sentence::AskResetEmailAlreadyResetError)
}
@ -484,6 +481,9 @@ pub async fn ask_reset_password_post(
AskResetPasswordError::UnableSendEmail(_) => {
context.tr.t(Sentence::UnableToSendResetEmail)
}
_ => "",
},
message: match error {
AskResetPasswordError::DatabaseError(_) => {
context.tr.t(Sentence::DatabaseError)
}
@ -853,7 +853,12 @@ pub async fn edit_user_post(
Ok(db::user::UpdateUserResult::EmailAlreadyTaken) => {
return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, context);
}
Ok(db::user::UpdateUserResult::UserUpdatedWaitingForRevalidation(token)) => {
Ok(db::user::UpdateUserResult::UserUpdatedWaitingForRevalidation(
token,
old_email,
old_token,
old_token_datetime,
)) => {
let url = utils::get_url_from_host(&host);
let email = form_data.email.clone();
match email_service
@ -874,6 +879,22 @@ pub async fn edit_user_post(
message = context.tr.t(Sentence::ProfileEmailSent);
}
Err(error) => {
// If the email can't be set we revert the changes about email and token.
if let Err(error) = connection
.update_user_email_and_token(
user.id,
&old_email,
old_token.as_deref(),
&old_token_datetime,
)
.await
{
error!(
"Unable to set email and token: (email={}): {}",
email, error
);
}
return error_response(
ProfileUpdateError::UnableToSendEmail(error),
&form_data,

View file

@ -8,7 +8,7 @@
<input id="email_field" type="email"
name="email" value="{{ email }}"
autocapitalize="none" autocomplete="email" autofocus="autofocus">
<span class="user-message">{{ message_email }}</span>
<span class="user-message ">{{ message_email }}</span>
<input type="submit" name="commit" value="{{ context.tr.t(Sentence::AskResetButton) }}">
</form>

View file

@ -6,17 +6,18 @@
<form action="/reset_password" method="post">
<label for="password_field_1">{{ context.tr.tp(Sentence::AskResetChooseNewPassword, [Box::new(common::consts::MIN_PASSWORD_SIZE)]) }}</label>
<input id="password_field_1" type="password" name="password_1">
<span></span>
<label for="password_field_1">{{ context.tr.t(Sentence::ReEnterPassword) }}</label>
<input id="password_field_2" type="password" name="password_2">
{{ message_password }}
<span class="user-message">{{ message_password }}</span>
<input type="hidden" name="reset_token" value="{{ reset_token }}">
<input type="submit" name="commit" value="Reset password">
<input type="submit" name="commit" value="{{ context.tr.t(Sentence::AskResetSubmit) }}">
</form>
{{ message }}
<span class="user-message">{{ message }}</span>
</div>
{% endblock %}

View file

@ -63,6 +63,7 @@
(AskResetEmailSent, "An email has been sent, follow the link to reset your password"),
(AskResetTokenMissing, "Reset token missing"),
(AskResetTokenExpired, "Token expired, try to reset password again"),
(AskResetSubmit, "Reset password"),
(PasswordReset, "Your password has been reset"),
(EmailUnknown, "Email unknown"),
(UnableToSendResetEmail, "Unable to send the reset password email"),

View file

@ -63,6 +63,7 @@
(AskResetEmailSent, "Un email a été envoyé, suivez le lien pour réinitialiser votre mot de passe"),
(AskResetTokenMissing, "Jeton de réinitialisation manquant"),
(AskResetTokenExpired, "Jeton expiré, essayez de réinitialiser votre mot de passe à nouveau"),
(AskResetSubmit, "Réinitialiser le mot de passe"),
(PasswordReset, "Votre mot de passe a été réinitialisé"),
(EmailUnknown, "Email inconnu"),
(UnableToSendResetEmail, "Impossible d'envoyer l'email pour la réinitialisation du mot de passe"),

View file

@ -68,6 +68,7 @@ pub enum Sentence {
AskResetEmailSent,
AskResetTokenMissing,
AskResetTokenExpired,
AskResetSubmit,
PasswordReset,
EmailUnknown,
UnableToSendResetEmail,