rails_audit_log 0.8.0 → 1.0.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 +4 -4
- data/README.md +294 -55
- data/Rakefile +5 -0
- data/app/assets/stylesheets/rails_audit_log/_01_base.css +32 -0
- data/app/assets/stylesheets/rails_audit_log/_02_layout.css +34 -0
- data/app/assets/stylesheets/rails_audit_log/_03_table.css +39 -0
- data/app/assets/stylesheets/rails_audit_log/_04_badges.css +13 -0
- data/app/assets/stylesheets/rails_audit_log/_05_diff.css +94 -0
- data/app/assets/stylesheets/rails_audit_log/_06_timeline.css +21 -0
- data/app/assets/stylesheets/rails_audit_log/_07_detail.css +15 -0
- data/app/assets/stylesheets/rails_audit_log/_08_pagination.css +56 -0
- data/app/assets/stylesheets/rails_audit_log/_09_filters.css +54 -0
- data/app/assets/stylesheets/rails_audit_log/application.css +1 -15
- data/app/concerns/rails_audit_log/auditable.rb +57 -0
- data/app/concerns/rails_audit_log/controller.rb +29 -0
- data/app/controllers/rails_audit_log/application_controller.rb +13 -0
- data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +32 -0
- data/app/controllers/rails_audit_log/resources_controller.rb +14 -0
- data/app/helpers/rails_audit_log/application_helper.rb +14 -0
- data/app/javascript/rails_audit_log/application.js +8 -0
- data/app/javascript/rails_audit_log/diff_controller.js +20 -0
- data/app/javascript/rails_audit_log/search_controller.js +12 -0
- data/app/jobs/rails_audit_log/application_job.rb +1 -0
- data/app/models/rails_audit_log/application_record.rb +1 -0
- data/app/models/rails_audit_log/audit_log_entry.rb +127 -25
- data/app/views/layouts/rails_audit_log/application.html.erb +16 -10
- data/app/views/rails_audit_log/audit_log_entries/_diff.html.erb +55 -0
- data/app/views/rails_audit_log/audit_log_entries/index.html.erb +76 -0
- data/app/views/rails_audit_log/audit_log_entries/show.html.erb +50 -0
- data/app/views/rails_audit_log/resources/show.html.erb +35 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +10 -0
- data/lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb +21 -0
- data/lib/generators/rails_audit_log/migrate_from_paper_trail/templates/migrate_from_paper_trail.rb +99 -0
- data/lib/rails_audit_log/engine.rb +29 -0
- data/lib/rails_audit_log/matchers.rb +56 -0
- data/lib/rails_audit_log/minitest_assertions.rb +36 -0
- data/lib/rails_audit_log/paper_trail_compat.rb +76 -0
- data/lib/rails_audit_log/test_helpers.rb +20 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +173 -5
- metadata +70 -5
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* Toggle */
|
|
2
|
+
.ral-diff-toggle {
|
|
3
|
+
display: flex;
|
|
4
|
+
border: 1px solid var(--ral-border);
|
|
5
|
+
border-radius: var(--ral-radius);
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
width: fit-content;
|
|
8
|
+
margin-bottom: 12px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.ral-diff-btn {
|
|
12
|
+
padding: 4px 12px;
|
|
13
|
+
font-size: 12px;
|
|
14
|
+
font-weight: 500;
|
|
15
|
+
color: var(--ral-muted);
|
|
16
|
+
background: var(--ral-surface);
|
|
17
|
+
border: none;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
}
|
|
20
|
+
.ral-diff-btn + .ral-diff-btn { border-left: 1px solid var(--ral-border); }
|
|
21
|
+
.ral-diff-btn:hover:not(.ral-diff-btn--active) { background: var(--ral-bg); color: var(--ral-text); }
|
|
22
|
+
.ral-diff-btn--active { background: var(--ral-primary); color: #fff; }
|
|
23
|
+
|
|
24
|
+
/* Inline table */
|
|
25
|
+
.ral-diff { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
26
|
+
|
|
27
|
+
.ral-diff th {
|
|
28
|
+
background: var(--ral-bg);
|
|
29
|
+
padding: 6px 12px;
|
|
30
|
+
text-align: left;
|
|
31
|
+
font-size: 11px;
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
text-transform: uppercase;
|
|
34
|
+
letter-spacing: 0.04em;
|
|
35
|
+
color: var(--ral-muted);
|
|
36
|
+
border-bottom: 1px solid var(--ral-border);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.ral-diff td { padding: 6px 12px; border-bottom: 1px solid var(--ral-border-subtle); vertical-align: top; }
|
|
40
|
+
.ral-diff tbody tr:last-child td { border-bottom: none; }
|
|
41
|
+
|
|
42
|
+
.ral-diff__attr { font-weight: 500; color: #333; width: 160px; }
|
|
43
|
+
.ral-diff__before { color: var(--ral-danger); background: #fff5f5; font-family: monospace; }
|
|
44
|
+
.ral-diff__after { color: var(--ral-success); background: #f0fdf4; font-family: monospace; }
|
|
45
|
+
|
|
46
|
+
/* Side-by-side panels */
|
|
47
|
+
.ral-diff-side {
|
|
48
|
+
display: grid;
|
|
49
|
+
grid-template-columns: 1fr 1fr;
|
|
50
|
+
border: 1px solid var(--ral-border);
|
|
51
|
+
border-radius: var(--ral-radius);
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
font-size: 13px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.ral-diff-side__panel--before { border-right: 1px solid var(--ral-border); }
|
|
57
|
+
|
|
58
|
+
.ral-diff-side__header {
|
|
59
|
+
padding: 6px 12px;
|
|
60
|
+
font-size: 11px;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
text-transform: uppercase;
|
|
63
|
+
letter-spacing: 0.04em;
|
|
64
|
+
color: var(--ral-muted);
|
|
65
|
+
border-bottom: 1px solid var(--ral-border);
|
|
66
|
+
background: var(--ral-bg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ral-diff-side__row {
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: 8px;
|
|
72
|
+
padding: 6px 12px;
|
|
73
|
+
border-bottom: 1px solid var(--ral-border-subtle);
|
|
74
|
+
align-items: baseline;
|
|
75
|
+
}
|
|
76
|
+
.ral-diff-side__row:last-child { border-bottom: none; }
|
|
77
|
+
|
|
78
|
+
.ral-diff-side__attr { font-weight: 500; color: #333; min-width: 100px; flex-shrink: 0; }
|
|
79
|
+
|
|
80
|
+
.ral-diff-side__panel--before .ral-diff-side__value {
|
|
81
|
+
color: var(--ral-danger);
|
|
82
|
+
font-family: monospace;
|
|
83
|
+
background: #fff5f5;
|
|
84
|
+
padding: 1px 4px;
|
|
85
|
+
border-radius: 3px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.ral-diff-side__panel--after .ral-diff-side__value {
|
|
89
|
+
color: var(--ral-success);
|
|
90
|
+
font-family: monospace;
|
|
91
|
+
background: #f0fdf4;
|
|
92
|
+
padding: 1px 4px;
|
|
93
|
+
border-radius: 3px;
|
|
94
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.ral-timeline { display: flex; flex-direction: column; gap: 16px; }
|
|
2
|
+
|
|
3
|
+
.ral-timeline__entry {
|
|
4
|
+
background: var(--ral-surface);
|
|
5
|
+
border: 1px solid var(--ral-border);
|
|
6
|
+
border-radius: var(--ral-radius);
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
box-shadow: var(--ral-shadow);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.ral-timeline__header {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 12px;
|
|
15
|
+
padding: 10px 16px;
|
|
16
|
+
background: var(--ral-bg);
|
|
17
|
+
border-bottom: 1px solid var(--ral-border);
|
|
18
|
+
font-size: 13px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.ral-timeline__detail-link { margin-left: auto; }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.ral-card {
|
|
2
|
+
background: var(--ral-surface);
|
|
3
|
+
border: 1px solid var(--ral-border);
|
|
4
|
+
border-radius: var(--ral-radius);
|
|
5
|
+
padding: 16px;
|
|
6
|
+
margin-bottom: 20px;
|
|
7
|
+
box-shadow: var(--ral-shadow);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ral-meta { display: grid; gap: 8px; }
|
|
11
|
+
.ral-meta__row { display: flex; gap: 16px; font-size: 13px; }
|
|
12
|
+
.ral-meta__row dt { width: 80px; color: var(--ral-muted); flex-shrink: 0; }
|
|
13
|
+
.ral-meta__row dd { color: var(--ral-text); }
|
|
14
|
+
|
|
15
|
+
.ral-entry-nav { display: flex; gap: 8px; align-items: center; }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
nav.pagy.series-nav {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 4px;
|
|
5
|
+
margin-top: 16px;
|
|
6
|
+
font-size: 13px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
nav.pagy.series-nav a {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
min-width: 32px;
|
|
14
|
+
height: 32px;
|
|
15
|
+
padding: 0 8px;
|
|
16
|
+
border: 1px solid var(--ral-border);
|
|
17
|
+
border-radius: 4px;
|
|
18
|
+
background: var(--ral-surface);
|
|
19
|
+
color: var(--ral-primary);
|
|
20
|
+
text-decoration: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
nav.pagy.series-nav a:hover:not([aria-disabled="true"]) { background: var(--ral-bg); }
|
|
24
|
+
|
|
25
|
+
nav.pagy.series-nav a[aria-current="page"] {
|
|
26
|
+
background: var(--ral-primary);
|
|
27
|
+
color: #fff;
|
|
28
|
+
border-color: var(--ral-primary);
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
nav.pagy.series-nav a[aria-disabled="true"] {
|
|
33
|
+
color: #aaa;
|
|
34
|
+
cursor: default;
|
|
35
|
+
border-color: var(--ral-border-subtle);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
nav.pagy.series-nav a[role="separator"] {
|
|
39
|
+
border: none;
|
|
40
|
+
color: var(--ral-muted);
|
|
41
|
+
min-width: auto;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ral-pagination__link {
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
height: 32px;
|
|
48
|
+
padding: 0 10px;
|
|
49
|
+
border: 1px solid var(--ral-border);
|
|
50
|
+
border-radius: 4px;
|
|
51
|
+
background: var(--ral-surface);
|
|
52
|
+
color: var(--ral-primary);
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
font-size: 13px;
|
|
55
|
+
}
|
|
56
|
+
.ral-pagination__link:hover { background: var(--ral-bg); text-decoration: none; }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
.ral-filters {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 8px;
|
|
5
|
+
flex-wrap: wrap;
|
|
6
|
+
margin-bottom: 16px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.ral-filter-input {
|
|
10
|
+
padding: 5px 10px;
|
|
11
|
+
font-size: 13px;
|
|
12
|
+
border: 1px solid var(--ral-border);
|
|
13
|
+
border-radius: var(--ral-radius);
|
|
14
|
+
background: var(--ral-surface);
|
|
15
|
+
color: var(--ral-text);
|
|
16
|
+
min-width: 180px;
|
|
17
|
+
}
|
|
18
|
+
.ral-filter-input:focus { outline: 2px solid var(--ral-primary); outline-offset: -1px; }
|
|
19
|
+
|
|
20
|
+
.ral-filter-select {
|
|
21
|
+
padding: 5px 8px;
|
|
22
|
+
font-size: 13px;
|
|
23
|
+
border: 1px solid var(--ral-border);
|
|
24
|
+
border-radius: var(--ral-radius);
|
|
25
|
+
background: var(--ral-surface);
|
|
26
|
+
color: var(--ral-text);
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ral-period-filter {
|
|
31
|
+
display: flex;
|
|
32
|
+
border: 1px solid var(--ral-border);
|
|
33
|
+
border-radius: var(--ral-radius);
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
margin-left: auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ral-period-btn {
|
|
39
|
+
padding: 5px 10px;
|
|
40
|
+
font-size: 13px;
|
|
41
|
+
font-weight: 500;
|
|
42
|
+
color: var(--ral-muted);
|
|
43
|
+
background: var(--ral-surface);
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
}
|
|
46
|
+
.ral-period-btn + .ral-period-btn { border-left: 1px solid var(--ral-border); }
|
|
47
|
+
.ral-period-btn:hover:not(.ral-period-btn--active) {
|
|
48
|
+
background: var(--ral-bg);
|
|
49
|
+
color: var(--ral-text);
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
}
|
|
52
|
+
.ral-period-btn--active { background: var(--ral-primary); color: #fff; }
|
|
53
|
+
|
|
54
|
+
.ral-filters__clear { font-size: 13px; }
|
|
@@ -1,15 +1 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
-
* listed below.
|
|
4
|
-
*
|
|
5
|
-
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
-
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
-
*
|
|
8
|
-
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
-
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
-
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
-
* It is generally better to create a new file per style scope.
|
|
12
|
-
*
|
|
13
|
-
*= require_tree .
|
|
14
|
-
*= require_self
|
|
15
|
-
*/
|
|
1
|
+
/* Styles are loaded via the dashboard_stylesheets helper which globs _*.css files. */
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
|
+
# Include in any ActiveRecord model to automatically track +create+, +update+,
|
|
3
|
+
# and +destroy+ events as {AuditLogEntry} records.
|
|
4
|
+
#
|
|
5
|
+
# == Basic usage
|
|
6
|
+
#
|
|
7
|
+
# class Article < ApplicationRecord
|
|
8
|
+
# include RailsAuditLog::Auditable
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# == Configuring tracking
|
|
12
|
+
#
|
|
13
|
+
# class Article < ApplicationRecord
|
|
14
|
+
# include RailsAuditLog::Auditable
|
|
15
|
+
# audit_log only: %i[title body],
|
|
16
|
+
# meta: { tenant_id: -> { Current.tenant_id } },
|
|
17
|
+
# version_limit: 50,
|
|
18
|
+
# async: true
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Adds a polymorphic +has_many :audit_log_entries+ association to the model.
|
|
2
22
|
module Auditable
|
|
3
23
|
extend ActiveSupport::Concern
|
|
4
24
|
|
|
@@ -12,6 +32,8 @@ module RailsAuditLog
|
|
|
12
32
|
|
|
13
33
|
_warn_if_audit_table_missing
|
|
14
34
|
|
|
35
|
+
# All {AuditLogEntry} records for this object, newest first by default.
|
|
36
|
+
# Destroyed when the object is destroyed.
|
|
15
37
|
has_many :audit_log_entries,
|
|
16
38
|
class_name: "RailsAuditLog::AuditLogEntry",
|
|
17
39
|
as: :item,
|
|
@@ -54,6 +76,7 @@ module RailsAuditLog
|
|
|
54
76
|
end
|
|
55
77
|
|
|
56
78
|
class_methods do
|
|
79
|
+
# @api private
|
|
57
80
|
def _warn_if_audit_table_missing
|
|
58
81
|
return if connection.table_exists?("audit_log_entries")
|
|
59
82
|
|
|
@@ -66,6 +89,33 @@ module RailsAuditLog
|
|
|
66
89
|
# DB not reachable during this phase (e.g. before db:create) — skip the check
|
|
67
90
|
end
|
|
68
91
|
|
|
92
|
+
# Configures auditing options for this model. Call once in the class body
|
|
93
|
+
# after +include RailsAuditLog::Auditable+.
|
|
94
|
+
#
|
|
95
|
+
# @param only [Array<Symbol>, nil] whitelist of attributes to track;
|
|
96
|
+
# when set, all other attributes are ignored regardless of +ignore:+
|
|
97
|
+
# @param ignore [Array<Symbol>, nil] additional attributes to exclude on
|
|
98
|
+
# top of {RailsAuditLog.ignored_attributes}; ignored when +only:+ is set
|
|
99
|
+
# @param meta [Hash{Symbol => Proc}, nil] per-entry metadata; each value
|
|
100
|
+
# is a lambda called at write time — zero-argument lambdas receive no
|
|
101
|
+
# arguments, one-argument lambdas receive the record instance
|
|
102
|
+
# @param associations [Boolean, Array<Symbol>, nil] when +true+, tracks
|
|
103
|
+
# all +has_many+ and +has_and_belongs_to_many+ associations; pass an
|
|
104
|
+
# array of association names to track only specific ones
|
|
105
|
+
# @param version_limit [Integer, nil] maximum number of entries to retain
|
|
106
|
+
# per record; oldest entries are pruned after each write; overrides
|
|
107
|
+
# {RailsAuditLog.version_limit} for this model
|
|
108
|
+
# @param async [Boolean, nil] when +true+, writes are dispatched via
|
|
109
|
+
# +WriteAuditLogJob+; overrides {RailsAuditLog.async} for this model
|
|
110
|
+
# @return [void]
|
|
111
|
+
# @example
|
|
112
|
+
# class Article < ApplicationRecord
|
|
113
|
+
# include RailsAuditLog::Auditable
|
|
114
|
+
# audit_log only: %i[title body published_at],
|
|
115
|
+
# meta: { tenant_id: -> { Current.tenant_id } },
|
|
116
|
+
# associations: %i[tags],
|
|
117
|
+
# version_limit: 100
|
|
118
|
+
# end
|
|
69
119
|
def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, async: nil)
|
|
70
120
|
self._audit_log_only = only.map(&:to_s) if only
|
|
71
121
|
self._audit_log_ignore = ignore.map(&:to_s) if ignore
|
|
@@ -76,6 +126,13 @@ module RailsAuditLog
|
|
|
76
126
|
end
|
|
77
127
|
end
|
|
78
128
|
|
|
129
|
+
# Executes the block with audit logging disabled for this record's writes.
|
|
130
|
+
# A convenience wrapper around {RailsAuditLog.disable}.
|
|
131
|
+
#
|
|
132
|
+
# @yield executes the block without recording any audit entries
|
|
133
|
+
# @return [Object] the return value of the block
|
|
134
|
+
# @example Skip auditing during a bulk update
|
|
135
|
+
# post.skip_audit_log { post.update!(cached_at: Time.current) }
|
|
79
136
|
def skip_audit_log
|
|
80
137
|
RailsAuditLog.disable { yield }
|
|
81
138
|
end
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
|
+
# Include in +ApplicationController+ (or any controller) to automatically
|
|
3
|
+
# set and clear the current actor for every request, so that audit entries
|
|
4
|
+
# written during the request are tagged with the signed-in user.
|
|
5
|
+
#
|
|
6
|
+
# == Usage
|
|
7
|
+
#
|
|
8
|
+
# class ApplicationController < ActionController::Base
|
|
9
|
+
# include RailsAuditLog::Controller
|
|
10
|
+
# audit_log_actor { current_user }
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# The block passed to {audit_log_actor} is evaluated in controller instance
|
|
14
|
+
# context on every request via a +before_action+. The actor is cleared in an
|
|
15
|
+
# +after_action+ so it never leaks between requests.
|
|
16
|
+
#
|
|
17
|
+
# Request metadata (+remote_ip+, +user_agent+) is captured automatically when
|
|
18
|
+
# {RailsAuditLog.capture_request_metadata} is +true+.
|
|
2
19
|
module Controller
|
|
3
20
|
extend ActiveSupport::Concern
|
|
4
21
|
|
|
@@ -10,10 +27,22 @@ module RailsAuditLog
|
|
|
10
27
|
end
|
|
11
28
|
|
|
12
29
|
class_methods do
|
|
30
|
+
# Registers a block that resolves the current actor for each request.
|
|
31
|
+
# The block is evaluated in controller instance context, so any helper
|
|
32
|
+
# method available to the controller (e.g. +current_user+) can be used.
|
|
33
|
+
#
|
|
34
|
+
# @yield block evaluated in controller context; should return the actor
|
|
35
|
+
# @return [void]
|
|
36
|
+
# @example
|
|
37
|
+
# audit_log_actor { current_user }
|
|
38
|
+
#
|
|
39
|
+
# # With a one-argument lambda style (actor = the controller instance)
|
|
40
|
+
# audit_log_actor { |c| c.current_user }
|
|
13
41
|
def audit_log_actor(&block)
|
|
14
42
|
@audit_log_actor_block = block
|
|
15
43
|
end
|
|
16
44
|
|
|
45
|
+
# @api private
|
|
17
46
|
def audit_log_actor_block
|
|
18
47
|
@audit_log_actor_block
|
|
19
48
|
end
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
|
+
# @api private
|
|
2
3
|
class ApplicationController < ActionController::Base
|
|
4
|
+
include Pagy::Method
|
|
5
|
+
|
|
6
|
+
protect_from_forgery with: :exception
|
|
7
|
+
before_action :authenticate!
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def authenticate!
|
|
12
|
+
return unless (auth = RailsAuditLog.authenticate)
|
|
13
|
+
|
|
14
|
+
instance_exec(self, &auth) || request_http_basic_authentication("Audit Log")
|
|
15
|
+
end
|
|
3
16
|
end
|
|
4
17
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
# @api private
|
|
3
|
+
class AuditLogEntriesController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
set_filters
|
|
6
|
+
@pagy, @entries = pagy(filtered_scope)
|
|
7
|
+
@item_types = AuditLogEntry.distinct.order(:item_type).pluck(:item_type)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@entry = AuditLogEntry.find(params[:id])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def set_filters
|
|
17
|
+
@event = params[:event].presence_in(AuditLogEntry::EVENTS)
|
|
18
|
+
@item_type = params[:item_type].presence
|
|
19
|
+
@period = params[:period].presence_in(AuditLogEntry::PERIODS.keys)
|
|
20
|
+
@q = params[:q].presence
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def filtered_scope
|
|
24
|
+
scope = AuditLogEntry.order(created_at: :desc)
|
|
25
|
+
scope = scope.where(event: @event) if @event
|
|
26
|
+
scope = scope.where(item_type: @item_type) if @item_type
|
|
27
|
+
scope = scope.for_period(@period) if @period
|
|
28
|
+
scope = scope.where("whodunnit_snapshot LIKE ?", "%#{@q}%") if @q
|
|
29
|
+
scope
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
# @api private
|
|
3
|
+
class ResourcesController < ApplicationController
|
|
4
|
+
def show
|
|
5
|
+
@item_type = params[:item_type]
|
|
6
|
+
@item_id = params[:item_id]
|
|
7
|
+
@pagy, @entries = pagy(
|
|
8
|
+
AuditLogEntry
|
|
9
|
+
.where(item_type: @item_type, item_id: @item_id)
|
|
10
|
+
.order(created_at: :asc)
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
|
+
# @api private
|
|
2
3
|
module ApplicationHelper
|
|
4
|
+
def format_diff_value(value)
|
|
5
|
+
return "—" if value.nil?
|
|
6
|
+
return "#{value["type"]} ##{value["id"]}" if value.is_a?(Hash)
|
|
7
|
+
|
|
8
|
+
value.inspect
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def dashboard_stylesheets
|
|
12
|
+
dir = RailsAuditLog::Engine.root.join("app/assets/stylesheets/rails_audit_log")
|
|
13
|
+
dir.glob("_*.css").sort.map do |file|
|
|
14
|
+
stylesheet_link_tag("rails_audit_log/#{file.basename}", media: "all")
|
|
15
|
+
end.join("\n").html_safe
|
|
16
|
+
end
|
|
3
17
|
end
|
|
4
18
|
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "@hotwired/turbo"
|
|
2
|
+
import { Application } from "@hotwired/stimulus"
|
|
3
|
+
import SearchController from "rails_audit_log/search_controller"
|
|
4
|
+
import DiffController from "rails_audit_log/diff_controller"
|
|
5
|
+
|
|
6
|
+
const application = Application.start()
|
|
7
|
+
application.register("search", SearchController)
|
|
8
|
+
application.register("diff", DiffController)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class DiffController extends Controller {
|
|
4
|
+
static targets = ["inline", "side", "inlineBtn", "sideBtn"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.setMode(localStorage.getItem("ral-diff-mode") || "inline")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
setInline() { this.setMode("inline") }
|
|
11
|
+
setSide() { this.setMode("side") }
|
|
12
|
+
|
|
13
|
+
setMode(mode) {
|
|
14
|
+
localStorage.setItem("ral-diff-mode", mode)
|
|
15
|
+
this.inlineTarget.hidden = mode !== "inline"
|
|
16
|
+
this.sideTarget.hidden = mode !== "side"
|
|
17
|
+
this.inlineBtnTarget.classList.toggle("ral-diff-btn--active", mode === "inline")
|
|
18
|
+
this.sideBtnTarget.classList.toggle("ral-diff-btn--active", mode === "side")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class SearchController extends Controller {
|
|
4
|
+
filter() {
|
|
5
|
+
clearTimeout(this._timeout)
|
|
6
|
+
this._timeout = setTimeout(() => this.element.requestSubmit(), 300)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
select() {
|
|
10
|
+
this.element.requestSubmit()
|
|
11
|
+
}
|
|
12
|
+
}
|