Files
clinch/app/controllers/profiles_controller.rb
Dan Milne cc93f72f0a 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>
2026-05-02 23:52:12 +10:00

59 lines
1.9 KiB
Ruby

class ProfilesController < ApplicationController
def show
@user = Current.session.user
end
def update
@user = Current.session.user
if params[:user][:password].present?
# Updating password - requires current password
unless @user.authenticate(params[:user][:current_password])
@user.errors.add(:current_password, "is incorrect")
render :show, status: :unprocessable_entity
return
end
if @user.update(password_params)
SecurityMailer.password_changed(@user, **security_event_context).deliver_later
redirect_to profile_path, notice: "Password updated successfully."
else
render :show, status: :unprocessable_entity
end
elsif params[:user][:email_address].present?
# Updating email - requires current password (security: prevents account takeover)
unless @user.authenticate(params[:user][:current_password])
@user.errors.add(:current_password, "is required to change email")
render :show, status: :unprocessable_entity
return
end
old_email = @user.email_address
if @user.update(email_params)
new_email = @user.email_address
if old_email != new_email
context = security_event_context
[old_email, new_email].uniq.each do |recipient|
SecurityMailer.email_address_changed(@user, recipient: recipient, old_email: old_email, new_email: new_email, **context).deliver_later
end
end
redirect_to profile_path, notice: "Email updated successfully."
else
render :show, status: :unprocessable_entity
end
else
render :show, status: :unprocessable_entity
end
end
private
def email_params
params.require(:user).permit(:email_address)
end
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
end