Notify users out-of-band when security settings change
Previously only TOTP-enabled triggered an email. Every other security-relevant change — password change, TOTP disable, passkey add/remove, API key create/revoke, email address change, backup-code regeneration — happened silently, so an attacker on a stolen session could quietly drop 2FA or hijack the email with no signal to the account holder. Add SecurityMailer with one method per event. Each email carries the request IP, user-agent, and timestamp so the user can spot unfamiliar activity. Email-address changes notify both the old and new addresses with directional language; the old-address copy explicitly warns that whoever made the change can now receive password reset emails. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
11
app/views/security_mailer/_event_metadata.html.erb
Normal file
11
app/views/security_mailer/_event_metadata.html.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
<hr>
|
||||
<p>
|
||||
This action was recorded at <strong><%= @occurred_at.to_fs(:long) %></strong>
|
||||
from IP <strong><%= @ip %></strong>
|
||||
using <strong><%= @user_agent.presence || "an unknown client" %></strong>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you did <strong>not</strong> perform this action, reset your password
|
||||
immediately and contact your administrator.
|
||||
</p>
|
||||
7
app/views/security_mailer/_event_metadata.text.erb
Normal file
7
app/views/security_mailer/_event_metadata.text.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
This action was recorded at <%= @occurred_at.to_fs(:long) %>
|
||||
from IP <%= @ip %>
|
||||
using <%= @user_agent.presence || "an unknown client" %>.
|
||||
|
||||
If you did not perform this action, reset your password immediately
|
||||
and contact your administrator.
|
||||
8
app/views/security_mailer/api_key_created.html.erb
Normal file
8
app/views/security_mailer/api_key_created.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>
|
||||
A new API key (<strong><%= @api_key_name %></strong>) was just created
|
||||
on your Clinch account (<strong><%= @user.email_address %></strong>).
|
||||
</p>
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
6
app/views/security_mailer/api_key_created.text.erb
Normal file
6
app/views/security_mailer/api_key_created.text.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
Hello,
|
||||
|
||||
A new API key ("<%= @api_key_name %>") was just created on your Clinch
|
||||
account (<%= @user.email_address %>).
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
8
app/views/security_mailer/api_key_revoked.html.erb
Normal file
8
app/views/security_mailer/api_key_revoked.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>
|
||||
The API key <strong><%= @api_key_name %></strong> was just revoked
|
||||
on your Clinch account (<strong><%= @user.email_address %></strong>).
|
||||
</p>
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
6
app/views/security_mailer/api_key_revoked.text.erb
Normal file
6
app/views/security_mailer/api_key_revoked.text.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
Hello,
|
||||
|
||||
The API key "<%= @api_key_name %>" was just revoked on your Clinch
|
||||
account (<%= @user.email_address %>).
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
@@ -0,0 +1,9 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>
|
||||
A new set of two-factor backup codes was generated on your Clinch
|
||||
account (<strong><%= @user.email_address %></strong>).
|
||||
Any previous backup codes are now invalid.
|
||||
</p>
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
@@ -0,0 +1,6 @@
|
||||
Hello,
|
||||
|
||||
A new set of two-factor backup codes was generated on your Clinch account
|
||||
(<%= @user.email_address %>). Any previous backup codes are now invalid.
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
22
app/views/security_mailer/email_address_changed.html.erb
Normal file
22
app/views/security_mailer/email_address_changed.html.erb
Normal file
@@ -0,0 +1,22 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<% if @recipient == @new_email %>
|
||||
<p>
|
||||
The email address on your Clinch account is now
|
||||
<strong><%= @new_email %></strong>.
|
||||
It was previously <strong><%= @old_email %></strong>.
|
||||
</p>
|
||||
<% else %>
|
||||
<p>
|
||||
The email address on your Clinch account was changed away from this
|
||||
address (<strong><%= @old_email %></strong>) to
|
||||
<strong><%= @new_email %></strong>.
|
||||
</p>
|
||||
<p>
|
||||
If this was <strong>not</strong> you, contact your administrator
|
||||
immediately — whoever made the change can now receive password
|
||||
reset emails for the account.
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
14
app/views/security_mailer/email_address_changed.text.erb
Normal file
14
app/views/security_mailer/email_address_changed.text.erb
Normal file
@@ -0,0 +1,14 @@
|
||||
Hello,
|
||||
|
||||
<% if @recipient == @new_email %>
|
||||
The email address on your Clinch account is now <%= @new_email %>.
|
||||
It was previously <%= @old_email %>.
|
||||
<% else %>
|
||||
The email address on your Clinch account was changed away from this
|
||||
address (<%= @old_email %>) to <%= @new_email %>.
|
||||
|
||||
If this was not you, contact your administrator immediately — whoever
|
||||
made the change can now receive password reset emails for the account.
|
||||
<% end %>
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
8
app/views/security_mailer/passkey_added.html.erb
Normal file
8
app/views/security_mailer/passkey_added.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>
|
||||
A new passkey (<strong><%= @nickname %></strong>) was just added to your
|
||||
Clinch account (<strong><%= @user.email_address %></strong>).
|
||||
</p>
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
6
app/views/security_mailer/passkey_added.text.erb
Normal file
6
app/views/security_mailer/passkey_added.text.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
Hello,
|
||||
|
||||
A new passkey ("<%= @nickname %>") was just added to your Clinch account
|
||||
(<%= @user.email_address %>).
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
8
app/views/security_mailer/passkey_removed.html.erb
Normal file
8
app/views/security_mailer/passkey_removed.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>
|
||||
A passkey (<strong><%= @nickname %></strong>) was just removed from your
|
||||
Clinch account (<strong><%= @user.email_address %></strong>).
|
||||
</p>
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
6
app/views/security_mailer/passkey_removed.text.erb
Normal file
6
app/views/security_mailer/passkey_removed.text.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
Hello,
|
||||
|
||||
A passkey ("<%= @nickname %>") was just removed from your Clinch account
|
||||
(<%= @user.email_address %>).
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
8
app/views/security_mailer/password_changed.html.erb
Normal file
8
app/views/security_mailer/password_changed.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>
|
||||
The password on your Clinch account
|
||||
(<strong><%= @user.email_address %></strong>) was just changed.
|
||||
</p>
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
5
app/views/security_mailer/password_changed.text.erb
Normal file
5
app/views/security_mailer/password_changed.text.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
Hello,
|
||||
|
||||
The password on your Clinch account (<%= @user.email_address %>) was just changed.
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
8
app/views/security_mailer/totp_disabled.html.erb
Normal file
8
app/views/security_mailer/totp_disabled.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>
|
||||
Two-factor authentication was just <strong>disabled</strong> on your
|
||||
Clinch account (<strong><%= @user.email_address %></strong>).
|
||||
</p>
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
6
app/views/security_mailer/totp_disabled.text.erb
Normal file
6
app/views/security_mailer/totp_disabled.text.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
Hello,
|
||||
|
||||
Two-factor authentication was just disabled on your Clinch account
|
||||
(<%= @user.email_address %>).
|
||||
|
||||
<%= render "event_metadata" %>
|
||||
Reference in New Issue
Block a user