module DangerousAction
extend ActiveSupport::Concern
FRESH_WINDOW = 10.minutes
class ConfirmationMismatch < StandardError; end
class StaleAuthentication < StandardError; end
included do
rescue_from ConfirmationMismatch, with: :handle_confirmation_mismatch
rescue_from StaleAuthentication, with: :handle_stale_authentication
end
private
def perform_dangerous_action(action:, target:, expected_phrase:)
require_fresh_authentication!
require_confirmation!(expected_phrase)
event = AuditEvent.record!(
actor: current_admin,
action: action,
target: target,
metadata: { ip: request.remote_ip, phrase: expected_phrase }
)
result = yield
event.mark_succeeded!
result
rescue ConfirmationMismatch, StaleAuthentication
raise
rescue StandardError => e
event&.mark_failed!(e.message)
raise
end
def require_confirmation!(expected_phrase)
submitted = params[:confirmation].to_s.strip.downcase
expected = expected_phrase.to_s.strip.downcase
raise ConfirmationMismatch if submitted.blank? || submitted != expected
end
def require_fresh_authentication!
last = session[:reauthenticated_at]
raise StaleAuthentication if last.blank?
raise StaleAuthentication if Time.zone.parse(last) < FRESH_WINDOW.ago
end
def handle_confirmation_mismatch
redirect_back fallback_location: root_path,
alert: "The confirmation phrase did not match. Nothing was changed."
end
def handle_stale_authentication
redirect_to reauthenticate_path(return_to: request.fullpath),
alert: "Please confirm your password to continue."
end
end
class AuditEvent < ApplicationRecord
belongs_to :actor, polymorphic: true
belongs_to :target, polymorphic: true, optional: true
enum status: { pending: 0, succeeded: 1, failed: 2 }
validates :action, presence: true
def self.record!(actor:, action:, target: nil, metadata: {})
create!(
actor: actor,
action: action.to_s,
target: target,
status: :pending,
metadata: metadata,
occurred_at: Time.current
)
end
def mark_succeeded!
update_columns(status: statuses[:succeeded], resolved_at: Time.current)
end
def mark_failed!(reason)
update_columns(
status: statuses[:failed],
resolved_at: Time.current,
metadata: metadata.merge("error" => reason)
)
end
def readonly?
persisted? && (status_previously_changed? ? false : true) && !new_record? && !@allow_status_write
end
def with_status_write
@allow_status_write = true
yield
ensure
@allow_status_write = false
end
end
module Admin
class CustomersController < Admin::BaseController
include DangerousAction
before_action :set_customer, only: %i[confirm_destroy destroy]
def confirm_destroy
# Renders a form that echoes back the required phrase (the email)
# and forces a password re-entry when auth is stale.
end
def destroy
perform_dangerous_action(
action: "customers.destroy",
target: @customer,
expected_phrase: @customer.email
) do
ActiveRecord::Base.transaction do
@customer.purge_personal_data!
@customer.orders.find_each(&:anonymize!)
@customer.destroy!
end
end
redirect_to admin_customers_path,
notice: "Customer #{@customer.email} was permanently deleted."
end
private
def set_customer
@customer = Customer.find(params[:id])
end
end
end
Destructive admin operations — deleting a customer, purging PII, force-refunding an order — are the kind of action that should never happen by accident or by a single misclick. This snippet shows a small, reusable pattern in Rails that forces every dangerous action through the same gate: an explicit typed confirmation phrase, a re-authentication check, and a durable audit record, all before the operation runs.
The DangerousAction concern centralizes the guard so controllers don't each reinvent it. require_confirmation! compares the submitted confirmation param against an expected_phrase (case- and whitespace-normalized) and raises ConfirmationMismatch when it doesn't match; require_fresh_authentication! enforces that the admin re-entered their password within FRESH_WINDOW, which mitigates session-hijack and unattended-terminal risk. The perform_dangerous_action wrapper ties it together: it verifies both guards, records an AuditEvent in a transaction, yields the actual work, and marks the event succeeded or failed. Because the audit row is written before the block runs and updated after, a crash mid-operation still leaves a pending/failed trail rather than silence.
The AuditEvent model is deliberately append-only: readonly? returns true once persisted so existing rows can't be mutated or destroyed through ActiveRecord, and record! captures who did what, against which target, with a JSON metadata blob for context. This is the tamper-resistant part of the pattern — an audit log that can be edited is not really an audit log.
In Admin::CustomersController, the destroy action reads like ordinary Rails code, but the real teardown is nested inside perform_dangerous_action with the target and the expected phrase (the customer's own email). The rescue_from handlers translate ConfirmationMismatch and StaleAuthentication into user-facing flashes rather than 500s.
The trade-off is friction: admins must retype a phrase and their password for high-blast-radius actions, which is intentional for irreversible operations but would be overkill for routine edits. Reserve this gate for actions that are hard or impossible to undo. A subtle pitfall is choosing a good expected_phrase — using a unique identifier like the record's email prevents muscle-memory confirmations from firing against the wrong record.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.