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>
44 lines
1.3 KiB
Ruby
44 lines
1.3 KiB
Ruby
class ApplicationController < ActionController::Base
|
|
include Authentication
|
|
|
|
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
|
allow_browser versions: :modern
|
|
|
|
# Changes to the importmap will invalidate the etag for HTML responses
|
|
stale_when_importmap_changes
|
|
|
|
# CSRF protection
|
|
protect_from_forgery with: :exception
|
|
|
|
helper_method :remove_query_param
|
|
|
|
private
|
|
|
|
def security_event_context
|
|
{ip: request.remote_ip, user_agent: request.user_agent, occurred_at: Time.current}
|
|
end
|
|
|
|
# Remove a query parameter from a URL using proper URI parsing
|
|
# More robust than regex - handles URL encoding, edge cases, etc.
|
|
#
|
|
# @param url [String] The URL to modify
|
|
# @param param_name [String] The query parameter name to remove
|
|
# @return [String] The URL with the parameter removed
|
|
#
|
|
# @example
|
|
# remove_query_param("https://example.com?foo=bar&baz=qux", "foo")
|
|
# # => "https://example.com?baz=qux"
|
|
def remove_query_param(url, param_name)
|
|
uri = URI.parse(url)
|
|
return url unless uri.query
|
|
|
|
params = Rack::Utils.parse_query(uri.query)
|
|
params.delete(param_name)
|
|
|
|
uri.query = params.any? ? Rack::Utils.build_query(params) : nil
|
|
uri.to_s
|
|
rescue URI::InvalidURIError
|
|
url
|
|
end
|
|
end
|