Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,27 @@ pub async fn send_verification_email(

send_via_resend(to, "Verify your Voltius email", html, text).await
}

pub async fn send_password_changed_email(
to: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let account_url = format!("{}/account", app_url().trim_end_matches('/'));
let when = chrono::Utc::now().format("%Y-%m-%d %H:%M UTC").to_string();
let html = branded_email_html(
"The master password of your Voltius account was just changed.",
"Security notice",
"Your master password was changed",
&format!(
r#"<p style="margin:0 0 14px;">The master password of your Voltius account was changed on {when}.</p>
<p style="margin:0;">If you did this, there is nothing else to do. If you did not, someone with access to one of your signed-in devices changed it: write to <a href="mailto:contact@voltius.app" style="color:#22d3ee;text-decoration:none;">contact@voltius.app</a> right away.</p>"#
),
"Open your account",
&account_url,
"Every other device signed in to this account has been signed out and needs the new password.",
);
let text = format!(
"The master password of your Voltius account was changed on {when}.\n\nIf you did this, there is nothing else to do. If you did not, write to contact@voltius.app right away.\n\nEvery other device signed in to this account has been signed out and needs the new password."
);

send_via_resend(to, "Your Voltius master password was changed", html, text).await
}
16 changes: 12 additions & 4 deletions src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::auth::{
password::{hash_auth_key, verify_auth_key},
AuthUser,
};
use crate::email::send_verification_email;
use crate::email::{send_password_changed_email, send_verification_email};
use crate::routes::email_undeliverable_response;
use crate::routes::invitations::{accept_pending_invitation, PendingInvitation, PENDING_INVITATION_COLUMNS};
use crate::self_host;
Expand Down Expand Up @@ -937,13 +937,13 @@ pub async fn update_password(
StatusCode::INTERNAL_SERVER_ERROR
})?;

sqlx::query(
"UPDATE users SET auth_hash = $1, wrapped_user_secrets = $2, token_version = token_version + 1, updated_at = now() WHERE id = $3",
let (email, undeliverable) = sqlx::query_as::<_, (String, bool)>(
"UPDATE users SET auth_hash = $1, wrapped_user_secrets = $2, token_version = token_version + 1, updated_at = now() WHERE id = $3 RETURNING email, email_undeliverable_at IS NOT NULL",
)
.bind(&new_auth_hash)
.bind(&body.new_wrapped_user_secrets)
.bind(auth.0)
.execute(&pool)
.fetch_one(&pool)
.await
.map_err(|e| {
error!(error = %e, user_id = %auth.0, "Failed to update password");
Expand All @@ -952,6 +952,14 @@ pub async fn update_password(

let tier = fetch_tier(&pool, auth.0).await?;
notifier.notify(auth.0, "token_invalidated".to_string());
if !undeliverable {
let user_id = auth.0;
tokio::spawn(async move {
if let Err(e) = send_password_changed_email(&email).await {
error!(error = %e, user_id = %user_id, "Failed to send password change notice");
}
});
}

info!(user_id = %auth.0, "User password updated");

Expand Down
Loading