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>
60 lines
2.2 KiB
Ruby
60 lines
2.2 KiB
Ruby
class SecurityMailer < ApplicationMailer
|
|
SUBJECT_PREFIX = "[Clinch security alert] ".freeze
|
|
|
|
def password_changed(user, ip:, user_agent:, occurred_at:)
|
|
assign_context(user, ip, user_agent, occurred_at)
|
|
mail subject: "#{SUBJECT_PREFIX}Your password was changed", to: user.email_address
|
|
end
|
|
|
|
def totp_disabled(user, ip:, user_agent:, occurred_at:)
|
|
assign_context(user, ip, user_agent, occurred_at)
|
|
mail subject: "#{SUBJECT_PREFIX}Two-factor authentication was disabled", to: user.email_address
|
|
end
|
|
|
|
def backup_codes_regenerated(user, ip:, user_agent:, occurred_at:)
|
|
assign_context(user, ip, user_agent, occurred_at)
|
|
mail subject: "#{SUBJECT_PREFIX}Two-factor backup codes were regenerated", to: user.email_address
|
|
end
|
|
|
|
def passkey_added(user, nickname:, ip:, user_agent:, occurred_at:)
|
|
assign_context(user, ip, user_agent, occurred_at)
|
|
@nickname = nickname
|
|
mail subject: "#{SUBJECT_PREFIX}A passkey was added to your account", to: user.email_address
|
|
end
|
|
|
|
def passkey_removed(user, nickname:, ip:, user_agent:, occurred_at:)
|
|
assign_context(user, ip, user_agent, occurred_at)
|
|
@nickname = nickname
|
|
mail subject: "#{SUBJECT_PREFIX}A passkey was removed from your account", to: user.email_address
|
|
end
|
|
|
|
def api_key_created(user, name:, ip:, user_agent:, occurred_at:)
|
|
assign_context(user, ip, user_agent, occurred_at)
|
|
@api_key_name = name
|
|
mail subject: "#{SUBJECT_PREFIX}An API key was created on your account", to: user.email_address
|
|
end
|
|
|
|
def api_key_revoked(user, name:, ip:, user_agent:, occurred_at:)
|
|
assign_context(user, ip, user_agent, occurred_at)
|
|
@api_key_name = name
|
|
mail subject: "#{SUBJECT_PREFIX}An API key was revoked on your account", to: user.email_address
|
|
end
|
|
|
|
def email_address_changed(user, recipient:, old_email:, new_email:, ip:, user_agent:, occurred_at:)
|
|
assign_context(user, ip, user_agent, occurred_at)
|
|
@recipient = recipient
|
|
@old_email = old_email
|
|
@new_email = new_email
|
|
mail subject: "#{SUBJECT_PREFIX}Your account email address was changed", to: recipient
|
|
end
|
|
|
|
private
|
|
|
|
def assign_context(user, ip, user_agent, occurred_at)
|
|
@user = user
|
|
@ip = ip
|
|
@user_agent = user_agent
|
|
@occurred_at = occurred_at
|
|
end
|
|
end
|