whodunit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.codeclimate.yml +50 -0
- data/.rubocop.yml +106 -0
- data/.yardopts +12 -0
- data/CHANGELOG.md +44 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +21 -0
- data/README.md +376 -0
- data/Rakefile +19 -0
- data/gemfiles/rails_7_2.gemfile +15 -0
- data/gemfiles/rails_8_2.gemfile +15 -0
- data/gemfiles/rails_edge.gemfile +15 -0
- data/lib/whodunit/controller_methods.rb +196 -0
- data/lib/whodunit/current.rb +78 -0
- data/lib/whodunit/migration_helpers.rb +229 -0
- data/lib/whodunit/railtie.rb +49 -0
- data/lib/whodunit/soft_delete_detector.rb +119 -0
- data/lib/whodunit/stampable.rb +184 -0
- data/lib/whodunit/version.rb +5 -0
- data/lib/whodunit.rb +121 -0
- data/sig/whodunit.rbs +4 -0
- metadata +254 -0
data/README.md
ADDED
@@ -0,0 +1,376 @@
|
|
1
|
+
# Whodunit
|
2
|
+
|
3
|
+
Lightweight creator/updater/deleter tracking for Rails ActiveRecord models.
|
4
|
+
|
5
|
+
> **Fun Fact**: The term "whodunit" was coined by literary critic Donald Gordon in 1930 when reviewing a murder mystery novel for _American News of Books_. He described Milward Kennedy's _Half Mast Murder_ as "a satisfactory whodunit" - the first recorded use of this now-famous term for mystery stories! _([Source: Wikipedia](https://en.wikipedia.org/wiki/Whodunit))_
|
6
|
+
|
7
|
+
## Overview
|
8
|
+
|
9
|
+
Whodunit provides simple auditing for Rails applications by tracking who created, updated, and deleted records. Unlike heavyweight solutions like PaperTrail or Audited, Whodunit focuses solely on user tracking with zero performance overhead.
|
10
|
+
|
11
|
+
## Requirements
|
12
|
+
|
13
|
+
- Ruby 3.1.1+ (tested on 3.1.1, 3.2.0, 3.3.0, 3.4). See the [the ruby-version matrix strategy of the CI workflow](https://github.com/kanutocd/whodunit/blob/main/.github/workflows/ci.yml#L15).
|
14
|
+
- Rails 7.2+ (tested on 7.2, 8.2, and edge)
|
15
|
+
- ActiveRecord for database operations
|
16
|
+
|
17
|
+
## Features
|
18
|
+
|
19
|
+
- **Lightweight**: Only tracks user IDs, no change history or versioning
|
20
|
+
- **Smart Soft-Delete Detection**: Automatically detects Discard, Paranoia, and custom soft-delete implementations
|
21
|
+
- **Thread-Safe**: Uses Rails `CurrentAttributes` pattern for user context
|
22
|
+
- **Zero Dependencies**: Only requires Rails 7.2+
|
23
|
+
- **Performance Focused**: No default scopes or method overrides
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
|
27
|
+
Add this line to your application's Gemfile:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
gem 'whodunit'
|
31
|
+
```
|
32
|
+
|
33
|
+
And then execute:
|
34
|
+
|
35
|
+
$ bundle install
|
36
|
+
|
37
|
+
## Quick Start
|
38
|
+
|
39
|
+
### 1. Add Stamp Columns
|
40
|
+
|
41
|
+
Generate a migration to add the tracking columns:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class AddStampsToUsers < ActiveRecord::Migration[7.0]
|
45
|
+
def change
|
46
|
+
add_whodunit_stamps :users # Adds creator_id, updater_id columns
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
For models with soft-delete, deleter tracking is automatically detected:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class AddStampsToDocuments < ActiveRecord::Migration[7.0]
|
55
|
+
def change
|
56
|
+
add_whodunit_stamps :documents # Adds creator_id, updater_id, deleter_id (if soft-delete detected)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
### 2. Include Stampable in Models
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class User < ApplicationRecord
|
65
|
+
include Whodunit::Stampable
|
66
|
+
end
|
67
|
+
|
68
|
+
class Document < ApplicationRecord
|
69
|
+
include Discard::Model # or acts_as_paranoid, etc.
|
70
|
+
include Whodunit::Stampable # Automatically detects soft-delete!
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
### 3. Set Up Controller Integration
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
class ApplicationController < ActionController::Base
|
78
|
+
# Whodunit::ControllerMethods is automatically included via Railtie
|
79
|
+
# It will automatically set the current user for stamping
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
## Usage
|
84
|
+
|
85
|
+
Once set up, stamping happens automatically:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# Creating records
|
89
|
+
user = User.create!(name: "Ken")
|
90
|
+
# => Sets user.creator_id to current_user.id
|
91
|
+
|
92
|
+
# Updating records
|
93
|
+
user.update!(name: "Sophia")
|
94
|
+
# => Sets user.updater_id to current_user.id
|
95
|
+
|
96
|
+
# Soft deleting (if soft-delete gem is detected)
|
97
|
+
document.discard
|
98
|
+
# => Sets document.deleter_id to current_user.id
|
99
|
+
```
|
100
|
+
|
101
|
+
Access the stamp information via associations:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
user.creator # => User who created this record
|
105
|
+
user.updater # => User who last updated this record
|
106
|
+
user.deleter # => User who deleted this record (if soft-delete enabled)
|
107
|
+
```
|
108
|
+
|
109
|
+
## Smart Soft-Delete Detection
|
110
|
+
|
111
|
+
Whodunit automatically detects popular soft-delete solutions:
|
112
|
+
|
113
|
+
- **Discard** (`gem 'discard'`)
|
114
|
+
- **Paranoia** (`gem 'paranoia'`)
|
115
|
+
- **ActsAsParanoid** (`gem 'acts_as_paranoid'`)
|
116
|
+
- **Custom implementations** with timestamp columns like `deleted_at`, `discarded_at`, etc.
|
117
|
+
|
118
|
+
## Configuration
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
# config/initializers/whodunit.rb
|
122
|
+
Whodunit.configure do |config|
|
123
|
+
config.user_class = 'Account' # Default: 'User'
|
124
|
+
config.creator_column = :created_by_id # Default: :creator_id
|
125
|
+
config.updater_column = :updated_by_id # Default: :updater_id
|
126
|
+
config.deleter_column = :deleted_by_id # Default: :deleter_id
|
127
|
+
config.auto_detect_soft_delete = false # Default: true
|
128
|
+
|
129
|
+
# Column data type configuration
|
130
|
+
config.column_data_type = :integer # Default: :bigint (applies to all columns)
|
131
|
+
config.creator_column_type = :string # Default: nil (uses column_data_type)
|
132
|
+
config.updater_column_type = :uuid # Default: nil (uses column_data_type)
|
133
|
+
config.deleter_column_type = :integer # Default: nil (uses column_data_type)
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
### Data Type Configuration
|
138
|
+
|
139
|
+
By default, all stamp columns use `:bigint` data type. You can customize this in several ways:
|
140
|
+
|
141
|
+
- **Global**: Set `column_data_type` to change the default for all columns
|
142
|
+
- **Individual**: Set specific column types to override the global default
|
143
|
+
- **Per-migration**: Override types on a per-migration basis (see Migration Helpers)
|
144
|
+
|
145
|
+
## Manual User Setting
|
146
|
+
|
147
|
+
For background jobs, tests, or special scenarios:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
# Temporarily set user
|
151
|
+
Whodunit::Current.user = User.find(123)
|
152
|
+
MyModel.create!(name: "test") # Will be stamped with user 123
|
153
|
+
|
154
|
+
# Within a block
|
155
|
+
controller.with_whodunit_user(admin_user) do
|
156
|
+
Document.create!(title: "Admin Document")
|
157
|
+
end
|
158
|
+
|
159
|
+
# Disable stamping temporarily
|
160
|
+
controller.without_whodunit_user do
|
161
|
+
Document.create!(title: "System Document") # No stamps
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
## Migration Helpers
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
# Basic usage (uses configured data types)
|
169
|
+
class CreatePosts < ActiveRecord::Migration[7.0]
|
170
|
+
def change
|
171
|
+
create_table :posts do |t|
|
172
|
+
t.string :title
|
173
|
+
t.whodunit_stamps # Adds creator_id, updater_id with configured types
|
174
|
+
t.timestamps
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Custom data types per migration
|
180
|
+
class CreateUsers < ActiveRecord::Migration[7.0]
|
181
|
+
def change
|
182
|
+
create_table :users do |t|
|
183
|
+
t.string :email
|
184
|
+
t.whodunit_stamps include_deleter: true,
|
185
|
+
creator_type: :uuid,
|
186
|
+
updater_type: :string,
|
187
|
+
deleter_type: :integer
|
188
|
+
t.timestamps
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Add to existing table with custom types
|
194
|
+
class AddStampsToExistingTable < ActiveRecord::Migration[7.0]
|
195
|
+
def change
|
196
|
+
add_whodunit_stamps :existing_table,
|
197
|
+
include_deleter: :auto,
|
198
|
+
creator_type: :string,
|
199
|
+
updater_type: :uuid
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Mixed approach - some custom, some default
|
204
|
+
class CreateDocuments < ActiveRecord::Migration[7.0]
|
205
|
+
def change
|
206
|
+
create_table :documents do |t|
|
207
|
+
t.string :title
|
208
|
+
t.whodunit_stamps creator_type: :uuid # Only override creator, others use defaults
|
209
|
+
t.timestamps
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
### Data Type Options
|
216
|
+
|
217
|
+
Common data types you can use:
|
218
|
+
|
219
|
+
- `:bigint` (default) - 64-bit integer, suitable for large user bases
|
220
|
+
- `:integer` - 32-bit integer, suitable for smaller applications
|
221
|
+
- `:string` - For string-based user identifiers
|
222
|
+
- `:uuid` - For UUID-based user systems
|
223
|
+
- Any other Rails column type
|
224
|
+
|
225
|
+
## Controller Methods
|
226
|
+
|
227
|
+
Skip stamping for specific actions:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
class ApiController < ApplicationController
|
231
|
+
skip_whodunit_for :index, :show
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
Only stamp specific actions:
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
class ReadOnlyController < ApplicationController
|
239
|
+
whodunit_only_for :create, :update, :destroy
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
## Thread Safety
|
244
|
+
|
245
|
+
Whodunit uses Rails `CurrentAttributes` for thread-safe user context:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
# Each thread maintains its own user context
|
249
|
+
Thread.new { Whodunit::Current.user = user1; create_records }
|
250
|
+
Thread.new { Whodunit::Current.user = user2; create_records }
|
251
|
+
```
|
252
|
+
|
253
|
+
## Testing
|
254
|
+
|
255
|
+
In your tests, you can set the user context:
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
# RSpec
|
259
|
+
before do
|
260
|
+
Whodunit::Current.user = create(:user)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Or within specific tests
|
264
|
+
it "tracks creator" do
|
265
|
+
user = create(:user)
|
266
|
+
Whodunit::Current.user = user
|
267
|
+
|
268
|
+
post = create(:post)
|
269
|
+
expect(post.creator).to eq(user)
|
270
|
+
end
|
271
|
+
```
|
272
|
+
|
273
|
+
## Comparisons
|
274
|
+
|
275
|
+
| Feature | Whodunit | PaperTrail | Audited |
|
276
|
+
| --------------------- | -------- | ---------- | ------- |
|
277
|
+
| User tracking | ✅ | ✅ | ✅ |
|
278
|
+
| Change history | ❌ | ✅ | ✅ |
|
279
|
+
| Performance overhead | None | High | Medium |
|
280
|
+
| Soft-delete detection | ✅ | ❌ | ❌ |
|
281
|
+
| Setup complexity | Low | Medium | Medium |
|
282
|
+
|
283
|
+
## Documentation
|
284
|
+
|
285
|
+
Complete API documentation is available at: **[https://kanutocd.github.io/whodunit](https://kanutocd.github.io/whodunit)**
|
286
|
+
|
287
|
+
The documentation includes:
|
288
|
+
|
289
|
+
- Comprehensive API reference with examples
|
290
|
+
- Configuration options and their defaults
|
291
|
+
- Migration helper methods
|
292
|
+
- Controller integration patterns
|
293
|
+
- Advanced usage scenarios
|
294
|
+
|
295
|
+
To generate documentation locally:
|
296
|
+
|
297
|
+
```bash
|
298
|
+
bundle exec yard doc
|
299
|
+
open doc/index.html
|
300
|
+
```
|
301
|
+
|
302
|
+
## Development
|
303
|
+
|
304
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
305
|
+
|
306
|
+
To install this gem onto your local machine, run `bundle exec gem build whodunit.gemspec && gem install ./whodunit-*.gem`.
|
307
|
+
|
308
|
+
### Testing
|
309
|
+
|
310
|
+
```bash
|
311
|
+
# Run all tests
|
312
|
+
bundle exec rspec
|
313
|
+
|
314
|
+
# Run tests with coverage
|
315
|
+
COVERAGE=true bundle exec rspec
|
316
|
+
|
317
|
+
# Run RuboCop
|
318
|
+
bundle exec rubocop
|
319
|
+
|
320
|
+
# Run security audit
|
321
|
+
bundle exec bundle audit check --update
|
322
|
+
|
323
|
+
# Generate documentation
|
324
|
+
bundle exec yard doc
|
325
|
+
```
|
326
|
+
|
327
|
+
### Release Process
|
328
|
+
|
329
|
+
The gem uses automated CI/CD workflows:
|
330
|
+
|
331
|
+
- **CI**: Automatically runs tests, linting, and security checks on every push and PR
|
332
|
+
- **Release**: Supports both automatic releases (on GitHub release creation) and manual releases via workflow dispatch
|
333
|
+
- **Documentation**: Automatically deploys API documentation to GitHub Pages
|
334
|
+
|
335
|
+
To perform a release:
|
336
|
+
|
337
|
+
1. **Dry Run**: Test the release process without publishing
|
338
|
+
|
339
|
+
```bash
|
340
|
+
# Via GitHub Actions UI: Run "Release" workflow with dry_run=true
|
341
|
+
```
|
342
|
+
|
343
|
+
2. **Create Release**:
|
344
|
+
|
345
|
+
```bash
|
346
|
+
# Update version in lib/whodunit/version.rb
|
347
|
+
# Commit and push changes
|
348
|
+
# Create a GitHub release via UI or CLI
|
349
|
+
gh release create v0.1.0 --title "Release v0.1.0" --notes "Release notes here"
|
350
|
+
```
|
351
|
+
|
352
|
+
3. **Manual Release** (if needed):
|
353
|
+
```bash
|
354
|
+
# Via GitHub Actions UI: Run "Release" workflow with dry_run=false
|
355
|
+
```
|
356
|
+
|
357
|
+
## Contributing
|
358
|
+
|
359
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kanutocd/whodunit.
|
360
|
+
|
361
|
+
### Development Workflow
|
362
|
+
|
363
|
+
1. Fork the repository
|
364
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
365
|
+
3. Make your changes
|
366
|
+
4. Add tests for new functionality
|
367
|
+
5. Ensure all tests pass (`bundle exec rspec`)
|
368
|
+
6. Run RuboCop and fix any style issues (`bundle exec rubocop`)
|
369
|
+
7. Update documentation if needed
|
370
|
+
8. Commit your changes (`git commit -am 'Add amazing feature'`)
|
371
|
+
9. Push to the branch (`git push origin feature/amazing-feature`)
|
372
|
+
10. Open a Pull Request
|
373
|
+
|
374
|
+
## License
|
375
|
+
|
376
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
# Load YARD tasks
|
9
|
+
begin
|
10
|
+
require "yard"
|
11
|
+
YARD::Rake::YardocTask.new do |t|
|
12
|
+
t.files = ["lib/**/*.rb"]
|
13
|
+
t.options = %w[--output-dir doc --markup markdown]
|
14
|
+
end
|
15
|
+
rescue LoadError
|
16
|
+
# YARD not available
|
17
|
+
end
|
18
|
+
|
19
|
+
task default: :spec
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Rails 7.2 testing
|
6
|
+
gem "activerecord", "~> 7.2.0"
|
7
|
+
gem "activesupport", "~> 7.2.0"
|
8
|
+
|
9
|
+
# Specify your gem's dependencies in whodunit.gemspec
|
10
|
+
gemspec path: "../"
|
11
|
+
|
12
|
+
gem "irb"
|
13
|
+
gem "rake", "~> 13.0"
|
14
|
+
|
15
|
+
gem "rspec", "~> 3.0"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Rails 8.0.2 testing
|
6
|
+
gem "activerecord", "~> 8.0.0"
|
7
|
+
gem "activesupport", "~> 8.0.0"
|
8
|
+
|
9
|
+
# Specify your gem's dependencies in whodunit.gemspec
|
10
|
+
gemspec path: "../"
|
11
|
+
|
12
|
+
gem "irb"
|
13
|
+
gem "rake", "~> 13.0"
|
14
|
+
|
15
|
+
gem "rspec", "~> 3.0"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Rails edge testing
|
6
|
+
gem "activerecord", git: "https://github.com/rails/rails.git"
|
7
|
+
gem "activesupport", git: "https://github.com/rails/rails.git"
|
8
|
+
|
9
|
+
# Specify your gem's dependencies in whodunit.gemspec
|
10
|
+
gemspec path: "../"
|
11
|
+
|
12
|
+
gem "irb"
|
13
|
+
gem "rake", "~> 13.0"
|
14
|
+
|
15
|
+
gem "rspec", "~> 3.0"
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Whodunit
|
4
|
+
# Controller integration for automatic user setting.
|
5
|
+
#
|
6
|
+
# This module provides seamless integration with Rails controllers by automatically
|
7
|
+
# setting and resetting the current user context for auditing purposes. It includes
|
8
|
+
# methods for manual user management and supports various user detection patterns.
|
9
|
+
#
|
10
|
+
# @example Automatic integration (via Railtie)
|
11
|
+
# # This module is automatically included in ActionController::Base
|
12
|
+
# class PostsController < ApplicationController
|
13
|
+
# # Whodunit automatically tracks current_user for all actions
|
14
|
+
# def create
|
15
|
+
# @post = Post.create(params[:post])
|
16
|
+
# # @post.creator_id is automatically set to current_user.id
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example Manual user setting
|
21
|
+
# class PostsController < ApplicationController
|
22
|
+
# def admin_action
|
23
|
+
# with_whodunit_user(User.admin) do
|
24
|
+
# # Actions here will be tracked as performed by admin user
|
25
|
+
# Post.create(title: "Admin Post")
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @example Skipping whodunit for specific actions
|
31
|
+
# class PostsController < ApplicationController
|
32
|
+
# skip_whodunit_for :index, :show
|
33
|
+
# # or
|
34
|
+
# whodunit_only_for :create, :update, :destroy
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @since 0.1.0
|
38
|
+
module ControllerMethods
|
39
|
+
extend ActiveSupport::Concern
|
40
|
+
|
41
|
+
included do
|
42
|
+
# Set up callbacks only for controllers that have user authentication
|
43
|
+
# Only if Rails controller methods are available
|
44
|
+
if respond_to?(:before_action) && respond_to?(:after_action)
|
45
|
+
before_action :set_whodunit_user, if: :whodunit_user_available?
|
46
|
+
after_action :reset_whodunit_user
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set the current user for whodunit tracking.
|
51
|
+
#
|
52
|
+
# This method is automatically called as a before_action in controllers.
|
53
|
+
# Can also be called manually to set a specific user context.
|
54
|
+
#
|
55
|
+
# @param user [Object, nil] the user object or user ID to set
|
56
|
+
# If nil, attempts to detect user via current_whodunit_user
|
57
|
+
# @return [void]
|
58
|
+
# @example Manual usage
|
59
|
+
# set_whodunit_user(User.find(123))
|
60
|
+
# set_whodunit_user(nil) # Uses current_whodunit_user detection
|
61
|
+
def set_whodunit_user(user = nil)
|
62
|
+
user ||= current_whodunit_user
|
63
|
+
Whodunit::Current.user = user
|
64
|
+
end
|
65
|
+
|
66
|
+
# Reset the current user context.
|
67
|
+
#
|
68
|
+
# This method is automatically called as an after_action in controllers
|
69
|
+
# to ensure proper cleanup between requests.
|
70
|
+
#
|
71
|
+
# @return [void]
|
72
|
+
def reset_whodunit_user
|
73
|
+
Whodunit::Current.reset
|
74
|
+
end
|
75
|
+
|
76
|
+
# Detect the current user for auditing.
|
77
|
+
#
|
78
|
+
# This method tries multiple strategies to find the current user:
|
79
|
+
# 1. current_user method (most common Rails pattern)
|
80
|
+
# 2. @current_user instance variable
|
81
|
+
# 3. Other common authentication method names
|
82
|
+
#
|
83
|
+
# Override this method in your controller to customize user detection.
|
84
|
+
#
|
85
|
+
# @return [Object, nil] the current user object or nil if none found
|
86
|
+
# @example Custom user detection
|
87
|
+
# def current_whodunit_user
|
88
|
+
# # Custom logic for finding user
|
89
|
+
# session[:admin_user] || super
|
90
|
+
# end
|
91
|
+
def current_whodunit_user
|
92
|
+
return current_user if respond_to?(:current_user) && current_user
|
93
|
+
return @current_user if defined?(@current_user) && @current_user
|
94
|
+
|
95
|
+
# Try other common patterns
|
96
|
+
user_methods = %i[
|
97
|
+
current_user current_account current_admin
|
98
|
+
signed_in_user authenticated_user
|
99
|
+
]
|
100
|
+
|
101
|
+
user_methods.each do |method|
|
102
|
+
return send(method) if respond_to?(method, true) && send(method)
|
103
|
+
end
|
104
|
+
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# Check if a user is available for stamping.
|
109
|
+
#
|
110
|
+
# Used internally by callbacks to determine if user tracking should occur.
|
111
|
+
#
|
112
|
+
# @return [Boolean] true if a user is available
|
113
|
+
def whodunit_user_available?
|
114
|
+
current_whodunit_user.present?
|
115
|
+
end
|
116
|
+
|
117
|
+
# Temporarily set a different user for a block of code.
|
118
|
+
#
|
119
|
+
# Useful for admin actions or background jobs that need to run
|
120
|
+
# as a specific user. Automatically restores the previous user context.
|
121
|
+
#
|
122
|
+
# @param user [Object] the user object to set temporarily
|
123
|
+
# @yield the block to execute with the temporary user
|
124
|
+
# @return [Object] the return value of the block
|
125
|
+
# @example
|
126
|
+
# with_whodunit_user(admin_user) do
|
127
|
+
# Post.create(title: "Admin post")
|
128
|
+
# end
|
129
|
+
def with_whodunit_user(user)
|
130
|
+
previous_user = Whodunit::Current.user
|
131
|
+
begin
|
132
|
+
Whodunit::Current.user = user
|
133
|
+
yield
|
134
|
+
ensure
|
135
|
+
Whodunit::Current.user = previous_user
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Temporarily disable user tracking for a block of code.
|
140
|
+
#
|
141
|
+
# Useful for system operations or bulk imports where user tracking
|
142
|
+
# is not desired.
|
143
|
+
#
|
144
|
+
# @yield the block to execute without user tracking
|
145
|
+
# @return [Object] the return value of the block
|
146
|
+
# @example
|
147
|
+
# without_whodunit_user do
|
148
|
+
# Post.bulk_import(data) # No creator_id will be set
|
149
|
+
# end
|
150
|
+
def without_whodunit_user(&block)
|
151
|
+
with_whodunit_user(nil, &block)
|
152
|
+
end
|
153
|
+
|
154
|
+
class_methods do
|
155
|
+
# Skip whodunit tracking for specific controller actions.
|
156
|
+
#
|
157
|
+
# Disables automatic user tracking for the specified actions.
|
158
|
+
# Useful for read-only actions where auditing is not needed.
|
159
|
+
#
|
160
|
+
# @param actions [Array<Symbol>] the action names to skip
|
161
|
+
# @return [void]
|
162
|
+
# @example
|
163
|
+
# class PostsController < ApplicationController
|
164
|
+
# skip_whodunit_for :index, :show
|
165
|
+
# end
|
166
|
+
def skip_whodunit_for(*actions)
|
167
|
+
return unless respond_to?(:skip_before_action) && respond_to?(:skip_after_action)
|
168
|
+
|
169
|
+
skip_before_action :set_whodunit_user, only: actions
|
170
|
+
skip_after_action :reset_whodunit_user, only: actions
|
171
|
+
end
|
172
|
+
|
173
|
+
# Enable whodunit tracking only for specific controller actions.
|
174
|
+
#
|
175
|
+
# Disables automatic tracking for all actions, then re-enables it
|
176
|
+
# only for the specified actions. Useful for controllers where
|
177
|
+
# only certain actions need auditing.
|
178
|
+
#
|
179
|
+
# @param actions [Array<Symbol>] the action names to enable tracking for
|
180
|
+
# @return [void]
|
181
|
+
# @example
|
182
|
+
# class PostsController < ApplicationController
|
183
|
+
# whodunit_only_for :create, :update, :destroy
|
184
|
+
# end
|
185
|
+
def whodunit_only_for(*actions)
|
186
|
+
return unless respond_to?(:skip_before_action) && respond_to?(:before_action) && respond_to?(:after_action)
|
187
|
+
|
188
|
+
skip_before_action :set_whodunit_user
|
189
|
+
skip_after_action :reset_whodunit_user
|
190
|
+
|
191
|
+
before_action :set_whodunit_user, only: actions, if: :whodunit_user_available?
|
192
|
+
after_action :reset_whodunit_user, only: actions
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|