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>
40 lines
1.4 KiB
Ruby
40 lines
1.4 KiB
Ruby
class PasswordsController < ApplicationController
|
|
allow_unauthenticated_access
|
|
before_action :set_user_by_token, only: %i[edit update]
|
|
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }
|
|
rate_limit to: 10, within: 10.minutes, only: :update, with: -> { redirect_to new_password_path, alert: "Too many attempts. Try again later." }
|
|
|
|
def new
|
|
end
|
|
|
|
def create
|
|
if (user = User.find_by(email_address: params[:email_address]))
|
|
PasswordsMailer.reset(user).deliver_later
|
|
end
|
|
|
|
redirect_to signin_path, notice: "Password reset instructions sent (if user with that email address exists)."
|
|
end
|
|
|
|
def edit
|
|
end
|
|
|
|
def update
|
|
if @user.update(params.permit(:password, :password_confirmation))
|
|
SecurityMailer.password_changed(@user, **security_event_context).deliver_later
|
|
@user.sessions.destroy_all
|
|
redirect_to signin_path, notice: "Password has been reset."
|
|
else
|
|
redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def set_user_by_token
|
|
@user = User.find_by_token_for(:password_reset, params[:token])
|
|
redirect_to new_password_path, alert: "Password reset link is invalid or has expired." if @user.nil?
|
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
|
|
end
|
|
end
|