active_element 0.0.2 → 0.0.4
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/.rubocop.yml +1 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +8 -9
- data/active_element.gemspec +1 -1
- data/app/assets/javascripts/active_element/application.js +3 -1
- data/app/assets/javascripts/active_element/popover.js +6 -0
- data/app/assets/javascripts/active_element/setup.js +12 -0
- data/app/assets/javascripts/active_element/{search_field.js → text_search_field.js} +2 -2
- data/app/assets/javascripts/active_element/toast.js +8 -0
- data/app/assets/stylesheets/active_element/application.scss +65 -1
- data/app/controllers/active_element/application_controller.rb +13 -19
- data/app/views/active_element/components/form/_label.html.erb +2 -2
- data/app/views/active_element/components/form/_templates.html.erb +8 -8
- data/app/views/active_element/components/form.html.erb +3 -3
- data/app/views/active_element/components/table/_collection_row.html.erb +3 -3
- data/app/views/active_element/components/table/collection.html.erb +1 -1
- data/app/views/active_element/components/table/item.html.erb +3 -3
- data/app/views/active_element/navbar/_menu.html.erb +2 -2
- data/app/views/layouts/active_element.html.erb +21 -23
- data/app/views/layouts/active_element_error.html.erb +1 -1
- data/config/routes.rb +10 -1
- data/lib/active_element/components/button.rb +6 -41
- data/lib/active_element/components/form.rb +12 -3
- data/lib/active_element/components/text_search/active_record_authorization.rb +13 -0
- data/lib/active_element/components/text_search/authorization.rb +117 -0
- data/lib/active_element/components/text_search/component.rb +118 -0
- data/lib/active_element/components/text_search/sql.rb +107 -0
- data/lib/active_element/components/text_search.rb +23 -0
- data/lib/active_element/components/util/decorator.rb +2 -2
- data/lib/active_element/components/util/record_path.rb +84 -0
- data/lib/active_element/components/util.rb +7 -2
- data/lib/active_element/components.rb +1 -0
- data/lib/active_element/controller_action.rb +9 -10
- data/lib/active_element/controller_interface.rb +78 -0
- data/lib/active_element/permissions_check.rb +33 -29
- data/lib/active_element/permissions_report.rb +57 -0
- data/lib/active_element/route.rb +1 -1
- data/lib/active_element/routes.rb +1 -1
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +24 -10
- data/lib/tasks/active_element.rake +1 -16
- data/rspec-documentation/pages/Components/Tables.md +2 -2
- data/rspec-documentation/spec_helper.rb +1 -1
- metadata +19 -12
- data/app/controllers/active_element/text_searches_controller.rb +0 -189
- data/lib/active_element/active_record_text_search_authorization.rb +0 -12
- data/lib/active_element/colorized_string.rb +0 -33
@@ -22,6 +22,7 @@ module ActiveElement
|
|
22
22
|
@modal = modal
|
23
23
|
@kwargs = kwargs
|
24
24
|
@columns = columns
|
25
|
+
@action = kwargs.delete(:action) { default_action }
|
25
26
|
@method = kwargs.delete(:method) { default_method }.to_s.downcase.to_sym
|
26
27
|
end
|
27
28
|
# rubocop:enable Metrics/MethodLength
|
@@ -30,7 +31,7 @@ module ActiveElement
|
|
30
31
|
'active_element/components/form'
|
31
32
|
end
|
32
33
|
|
33
|
-
def locals # rubocop:disable Metrics/MethodLength
|
34
|
+
def locals # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
34
35
|
{
|
35
36
|
component: self,
|
36
37
|
fields: Util::FormFieldMapping.new(record, fields, i18n).fields_with_types_and_options,
|
@@ -39,6 +40,7 @@ module ActiveElement
|
|
39
40
|
submit_position: submit_position,
|
40
41
|
class_name: class_name,
|
41
42
|
method: method,
|
43
|
+
action: action,
|
42
44
|
kwargs: kwargs,
|
43
45
|
destroy: destroy,
|
44
46
|
modal: modal,
|
@@ -130,7 +132,8 @@ module ActiveElement
|
|
130
132
|
|
131
133
|
private
|
132
134
|
|
133
|
-
attr_reader :fields, :submit, :title, :kwargs, :item, :method, :
|
135
|
+
attr_reader :fields, :submit, :title, :kwargs, :item, :method, :action,
|
136
|
+
:destroy, :modal, :expanded, :columns
|
134
137
|
|
135
138
|
def valid_field?(field)
|
136
139
|
return true if record.respond_to?("#{field}_changed?") && !record.public_send("#{field}_changed?")
|
@@ -197,7 +200,7 @@ module ActiveElement
|
|
197
200
|
|
198
201
|
def default_method
|
199
202
|
case controller.action_name
|
200
|
-
when 'edit'
|
203
|
+
when 'edit', 'update'
|
201
204
|
'PATCH'
|
202
205
|
when 'index'
|
203
206
|
'GET'
|
@@ -205,6 +208,12 @@ module ActiveElement
|
|
205
208
|
'POST'
|
206
209
|
end
|
207
210
|
end
|
211
|
+
|
212
|
+
def default_action
|
213
|
+
return controller.request.path unless record.is_a?(ActiveModel::Naming)
|
214
|
+
|
215
|
+
Util::RecordPath.new(record: record, controller: controller).path
|
216
|
+
end
|
208
217
|
end
|
209
218
|
end
|
210
219
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActiveRecord::Base.class_eval do
|
4
|
+
class << self
|
5
|
+
def authorize_active_element_text_search(with:, providing:)
|
6
|
+
ActiveElement::Components::TextSearch.register_authorized_text_search(
|
7
|
+
model: self,
|
8
|
+
with: with,
|
9
|
+
providing: providing
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module Components
|
5
|
+
module TextSearch
|
6
|
+
# Manages authorization for text search, ensures model is configured for text search and
|
7
|
+
# that user has correct permissions.
|
8
|
+
class Authorization
|
9
|
+
include Paintbrush
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def permission_for(model:, field:)
|
13
|
+
"can_text_search_#{application_name}_#{model.name.underscore.pluralize}_with_#{field}"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def application_name
|
19
|
+
RailsComponent.new(::Rails).application_name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(model:, params:, user:, search_columns:, result_columns:)
|
24
|
+
@model = model
|
25
|
+
@params = params
|
26
|
+
@user = user
|
27
|
+
@search_columns = search_columns
|
28
|
+
@result_columns = result_columns
|
29
|
+
end
|
30
|
+
|
31
|
+
def authorized?
|
32
|
+
return true if authorized_model? && authorized_user?
|
33
|
+
return false.tap { ActiveElement.warning(message) } unless Rails.env.development?
|
34
|
+
|
35
|
+
ActiveElement.warning(development_message)
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def message(colorize: true)
|
40
|
+
[
|
41
|
+
missing_authorization_message(colorize: colorize),
|
42
|
+
missing_permissions_message(colorize: colorize)
|
43
|
+
].compact.join(paintbrush(colorize: colorize) { red '. ' })
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :model, :params, :user, :search_columns, :result_columns
|
49
|
+
|
50
|
+
def development_message
|
51
|
+
paintbrush { green "Bypassed text search authorization in development environment: #{yellow message}" }
|
52
|
+
end
|
53
|
+
|
54
|
+
def missing_permissions
|
55
|
+
(search_columns + result_columns).reject { |column| user_permitted?(column) }
|
56
|
+
.map { |column| permission_for(column.name) }
|
57
|
+
.uniq
|
58
|
+
.sort
|
59
|
+
end
|
60
|
+
|
61
|
+
def missing_permissions_message(colorize:)
|
62
|
+
return nil if missing_permissions.empty?
|
63
|
+
|
64
|
+
paintbrush(colorize: colorize) { red "Missing permissions: #{yellow missing_permissions.join(', ')}" }
|
65
|
+
end
|
66
|
+
|
67
|
+
def missing_authorization_message(colorize:)
|
68
|
+
return nil if authorized_model?
|
69
|
+
|
70
|
+
paintbrush(colorize: colorize) do
|
71
|
+
red "Missing model authorization for #{cyan model_name} with: " \
|
72
|
+
"#{green search_fields.join(', ')}, providing: " \
|
73
|
+
"#{green result_fields.join(', ')}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def search_fields
|
78
|
+
params[:attributes]
|
79
|
+
end
|
80
|
+
|
81
|
+
def model_name
|
82
|
+
model&.name || params[:model]
|
83
|
+
end
|
84
|
+
|
85
|
+
def result_fields
|
86
|
+
(params[:attributes] + [params[:value]]).uniq
|
87
|
+
end
|
88
|
+
|
89
|
+
def user_permitted?(column)
|
90
|
+
user&.permissions&.include?(permission_for(column.name))
|
91
|
+
end
|
92
|
+
|
93
|
+
def authorized_user?
|
94
|
+
missing_permissions.empty?
|
95
|
+
end
|
96
|
+
|
97
|
+
def permission_for(field)
|
98
|
+
self.class.permission_for(model: model, field: field)
|
99
|
+
end
|
100
|
+
|
101
|
+
def authorized_model?
|
102
|
+
TextSearch.authorized_text_searches.any? do |authorized_model, search_fields, result_fields|
|
103
|
+
next false unless authorized_model == model
|
104
|
+
next false unless authorized_fields?(search_columns, Array(search_fields))
|
105
|
+
next false unless authorized_fields?(result_columns, Array(result_fields))
|
106
|
+
|
107
|
+
true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def authorized_fields?(columns, fields)
|
112
|
+
columns.all? { |column| fields.map(&:to_sym).include?(column.name.to_sym) }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module Components
|
5
|
+
module TextSearch
|
6
|
+
# Used by auto-complete search field for executing a text search on the provided model and
|
7
|
+
# attributes.
|
8
|
+
#
|
9
|
+
# The user must have a permission configured for each field used in the search:
|
10
|
+
# `can_text_search_<application_name>_<models>_with_<field>`
|
11
|
+
#
|
12
|
+
# A model must call `authorize_active_element_text_search` to enable text search. e.g.:
|
13
|
+
#
|
14
|
+
# class MyModel < ApplicationRecord
|
15
|
+
# authorize_active_element_text_search with: [:id, :email],
|
16
|
+
# providing: [:id, :first_name, :last_name, :email]
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# This allows searching using the `name` `email` columns and permits returning each matching
|
20
|
+
# record's `id`, `first_name`, `last_name`, and `email` values.
|
21
|
+
#
|
22
|
+
# This complexity exists to ensure that authenticated users can only retrieve specific
|
23
|
+
# database values that are explicitly configured, as well as ensuring that users cannot
|
24
|
+
# search arbitrary columns. Requiring this logic in the model is intended to reduce
|
25
|
+
# likelihood of DoS vulnerabilities if users are able to search unindexed columns.
|
26
|
+
#
|
27
|
+
# Note that the `/_active_element_text_search` endpoint added to each controller
|
28
|
+
# necessarily receives arbitrary arguments. Configuring a form to only fetch certain values
|
29
|
+
# does not restrict potential parameters, so a strict permissions and model configuration
|
30
|
+
# system is required to govern access to database queries.
|
31
|
+
#
|
32
|
+
class Component
|
33
|
+
DEFAULT_LIMIT = 50
|
34
|
+
|
35
|
+
def initialize(controller:)
|
36
|
+
@controller = controller
|
37
|
+
@params = controller.params
|
38
|
+
end
|
39
|
+
|
40
|
+
def response
|
41
|
+
return unverified_parameters unless verified_parameters?
|
42
|
+
return unverified_model unless verified_model?
|
43
|
+
return unauthorized unless authorization.authorized?
|
44
|
+
|
45
|
+
{ json: { results: results, request_id: controller.params[:request_id] }, status: :created }
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_reader :controller, :params
|
51
|
+
|
52
|
+
def verified_parameters?
|
53
|
+
%i[model attributes value query].all? { |parameter| params[parameter].present? }
|
54
|
+
end
|
55
|
+
|
56
|
+
def unverified_parameters
|
57
|
+
{ json: { message: 'Must provide parameters: [model, attributes, value, query] for text search.' },
|
58
|
+
status: :unprocessable_entity }
|
59
|
+
end
|
60
|
+
|
61
|
+
def verified_model?
|
62
|
+
[model, sql.search_columns, sql.value_column].all?(&:present?)
|
63
|
+
end
|
64
|
+
|
65
|
+
def unverified_model
|
66
|
+
{ json: { message: authorization.message(colorize: false) }, status: :unprocessable_entity }
|
67
|
+
end
|
68
|
+
|
69
|
+
def unauthorized
|
70
|
+
{ json: { message: authorization.message(colorize: false) }, status: :forbidden }
|
71
|
+
end
|
72
|
+
|
73
|
+
def sql
|
74
|
+
@sql ||= Sql.new(
|
75
|
+
model: model,
|
76
|
+
query: params[:query],
|
77
|
+
value: params[:value],
|
78
|
+
attributes: params[:attributes]
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def results
|
83
|
+
@results ||= model.where(*sql.whereclause)
|
84
|
+
.limit(limit)
|
85
|
+
.pluck(sql.value_column.name, *sql.search_columns.map(&:name))
|
86
|
+
.map { |value, *attributes| result(value, attributes) }
|
87
|
+
.uniq
|
88
|
+
end
|
89
|
+
|
90
|
+
def result(value, attributes)
|
91
|
+
{ value: value, attributes: attributes.reject { |attribute| attribute == value } }
|
92
|
+
end
|
93
|
+
|
94
|
+
def model
|
95
|
+
@model ||= params[:model].camelize(:upper).safe_constantize
|
96
|
+
end
|
97
|
+
|
98
|
+
def authorization
|
99
|
+
@authorization ||= TextSearch::Authorization.new(
|
100
|
+
model: model,
|
101
|
+
params: params,
|
102
|
+
user: controller.active_element.current_user,
|
103
|
+
search_columns: sql.search_columns.compact,
|
104
|
+
result_columns: (sql.search_columns + [sql.value_column]).compact
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def query
|
109
|
+
params[:query]
|
110
|
+
end
|
111
|
+
|
112
|
+
def limit
|
113
|
+
DEFAULT_LIMIT
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module Components
|
5
|
+
module TextSearch
|
6
|
+
# Encapsulates generation of database adapter-specific sanitized SQL queries for performing
|
7
|
+
# full text searches. Identifies columns and adapters where `LIKE` or `ILIKE` can be
|
8
|
+
# applied and generates a whereclause according to the provided parameters.
|
9
|
+
#
|
10
|
+
# Receives an ActiveRecord model class, a query (a string used to match results), attribute
|
11
|
+
# columns to match against, and a value column to return in the results.
|
12
|
+
#
|
13
|
+
# Inspects ActiveRecord metadata to match field names to column objects.
|
14
|
+
class Sql
|
15
|
+
def initialize(model:, query:, value:, attributes:)
|
16
|
+
@model = model
|
17
|
+
@query = query
|
18
|
+
@value = value
|
19
|
+
@attributes = attributes
|
20
|
+
end
|
21
|
+
|
22
|
+
def value_column
|
23
|
+
return nil if value.blank?
|
24
|
+
|
25
|
+
@value_column ||= model&.columns&.find { |column| column.name == value }
|
26
|
+
end
|
27
|
+
|
28
|
+
def search_columns
|
29
|
+
return [] if attributes.blank?
|
30
|
+
|
31
|
+
@search_columns ||= attributes.map { |attribute| column_for(attribute) }.compact
|
32
|
+
end
|
33
|
+
|
34
|
+
def whereclause
|
35
|
+
clauses = search_columns.map { |column| "#{column.name} #{operator(column)} ?" }
|
36
|
+
[clauses.join(' OR '), search_columns.map { |column| search_param(column) }].flatten
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :model, :query, :value, :attributes
|
42
|
+
|
43
|
+
def column_for(attribute)
|
44
|
+
matched_column = model&.columns&.find { |column| column.name == attribute }
|
45
|
+
return nil if matched_column.blank?
|
46
|
+
|
47
|
+
compatible_column?(matched_column) ? matched_column : nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def operator(column)
|
51
|
+
case column.type
|
52
|
+
when :string
|
53
|
+
%w[Mysql2 SQLite].include?(model.connection.adapter_name) ? 'LIKE' : 'ILIKE'
|
54
|
+
else
|
55
|
+
'='
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def compatible_column?(column) # rubocop:disable Metrics/MethodLength
|
60
|
+
case column.type
|
61
|
+
when :string
|
62
|
+
true
|
63
|
+
when :integer
|
64
|
+
integer?
|
65
|
+
when :float
|
66
|
+
float?
|
67
|
+
when :decimal
|
68
|
+
decimal?
|
69
|
+
else
|
70
|
+
Rails.logger.info("Skipping query `#{query}` for incompatible column: #{column.name}")
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def integer?
|
76
|
+
Integer(query)
|
77
|
+
true
|
78
|
+
rescue ArgumentError
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
def float?
|
83
|
+
Float(query)
|
84
|
+
true
|
85
|
+
rescue ArgumentError
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
def decimal?
|
90
|
+
BigDecimal(query)
|
91
|
+
true
|
92
|
+
rescue ArgumentError
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
96
|
+
def search_param(column)
|
97
|
+
case column.type
|
98
|
+
when :string
|
99
|
+
"#{query}%"
|
100
|
+
else
|
101
|
+
query
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'text_search/active_record_authorization'
|
4
|
+
require_relative 'text_search/sql'
|
5
|
+
require_relative 'text_search/component'
|
6
|
+
require_relative 'text_search/authorization'
|
7
|
+
|
8
|
+
module ActiveElement
|
9
|
+
module Components
|
10
|
+
# Provides back end for live text search components.
|
11
|
+
module TextSearch
|
12
|
+
@authorized_text_searches = []
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_reader :authorized_text_searches
|
16
|
+
|
17
|
+
def register_authorized_text_search(model:, with:, providing:)
|
18
|
+
authorized_text_searches << [model, with, providing]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -27,7 +27,7 @@ module ActiveElement
|
|
27
27
|
component.controller.render_to_string(partial: decorator_path(sti: sti), locals: locals)
|
28
28
|
rescue ActionView::MissingTemplate
|
29
29
|
if sti
|
30
|
-
component.controller.missing_template_store[decorator_path] = true
|
30
|
+
component.controller.active_element.missing_template_store[decorator_path] = true
|
31
31
|
default_decorated_value
|
32
32
|
else
|
33
33
|
render(sti: true)
|
@@ -51,7 +51,7 @@ module ActiveElement
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def missing_template?
|
54
|
-
component.controller.missing_template_store[decorator_path].present?
|
54
|
+
component.controller.active_element.missing_template_store[decorator_path].present?
|
55
55
|
end
|
56
56
|
|
57
57
|
def decorator_path(sti: false)
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module Components
|
5
|
+
module Util
|
6
|
+
# Generates Rails paths from records, used to generate form actions and links for Rails RESTful routes.
|
7
|
+
class RecordPath
|
8
|
+
def initialize(record:, controller:, type: nil)
|
9
|
+
@record = record
|
10
|
+
@controller = controller
|
11
|
+
@type = type&.to_sym || controller.action_name&.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def path
|
15
|
+
record_path || sti_record_path
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :record, :controller, :type
|
21
|
+
|
22
|
+
def namespace_prefix
|
23
|
+
# XXX: We guess the namespace from the current controller's module name. This will work
|
24
|
+
# most of the time but will break if the current record's controller exists in a different
|
25
|
+
# namespace to the current controller, e.g. `BackEndAdmin::UsersController` and
|
26
|
+
# `FrontEndAdmin::ThemesController` - if `FrontEndAdmin::ThemesController` renders a
|
27
|
+
# collection of `User` objects, the "show" path will be wrong:
|
28
|
+
# `front_end_admin_user_path`. Maybe descend through the full controller class tree to
|
29
|
+
# find a best match ?
|
30
|
+
namespace = controller.class.name.deconstantize.underscore
|
31
|
+
return nil if namespace.blank?
|
32
|
+
|
33
|
+
"#{namespace}_"
|
34
|
+
end
|
35
|
+
|
36
|
+
def record_path
|
37
|
+
return nil if record.nil?
|
38
|
+
|
39
|
+
controller.helpers.public_send(default_record_path, record)
|
40
|
+
rescue NoMethodError
|
41
|
+
controller.helpers.public_send(sti_record_path, record)
|
42
|
+
end
|
43
|
+
|
44
|
+
def default_record_path
|
45
|
+
"#{record_path_prefix}#{namespace_prefix}#{record_name}_path"
|
46
|
+
end
|
47
|
+
|
48
|
+
def sti_record_path
|
49
|
+
"#{record_path_prefix}#{namespace_prefix}#{sti_record_name}_path"
|
50
|
+
end
|
51
|
+
|
52
|
+
def record_name
|
53
|
+
return Util.record_name(record) unless pluralize?
|
54
|
+
|
55
|
+
Util.record_name(record)&.pluralize
|
56
|
+
end
|
57
|
+
|
58
|
+
def sti_record_name
|
59
|
+
return Util.sti_record_name(record) unless pluralize?
|
60
|
+
|
61
|
+
Util.sti_record_name(record)
|
62
|
+
end
|
63
|
+
|
64
|
+
def record_path_prefix
|
65
|
+
case type
|
66
|
+
when :edit, :update
|
67
|
+
'edit_'
|
68
|
+
when :new, :create
|
69
|
+
'new_'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def pluralize?
|
74
|
+
case type
|
75
|
+
when :index, :create
|
76
|
+
true
|
77
|
+
else
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'util/i18n'
|
4
|
+
require_relative 'util/record_path'
|
4
5
|
require_relative 'util/record_mapping'
|
5
6
|
require_relative 'util/field_mapping'
|
6
7
|
require_relative 'util/form_field_mapping'
|
@@ -18,15 +19,19 @@ module ActiveElement
|
|
18
19
|
end
|
19
20
|
|
20
21
|
def self.record_name(record)
|
21
|
-
record&.try(:model_name)&.try(&:singular) || record
|
22
|
+
record&.try(:model_name)&.try(&:singular) || default_record_name(record)
|
22
23
|
end
|
23
24
|
|
24
25
|
def self.sti_record_name(record)
|
25
|
-
return
|
26
|
+
return default_record_name(record) unless record.class.respond_to?(:inheritance_column)
|
26
27
|
|
27
28
|
record&.class&.superclass&.model_name&.singular if record&.try(record.class.inheritance_column).present?
|
28
29
|
end
|
29
30
|
|
31
|
+
def self.default_record_name(record)
|
32
|
+
record.class.name.demodulize.underscore
|
33
|
+
end
|
34
|
+
|
30
35
|
def self.json_pretty_print(json)
|
31
36
|
theme = Rouge::Themes::Base16.mode(:light)
|
32
37
|
formatter = Rouge::Formatters::HTMLLinewise.new(Rouge::Formatters::HTMLInline.new(theme))
|
@@ -12,6 +12,7 @@ require_relative 'components/button'
|
|
12
12
|
require_relative 'components/tabs'
|
13
13
|
require_relative 'components/tab'
|
14
14
|
require_relative 'components/json'
|
15
|
+
require_relative 'components/text_search'
|
15
16
|
|
16
17
|
module ActiveElement
|
17
18
|
# Standardised HTML components to be used in admin front ends.
|
@@ -3,15 +3,17 @@
|
|
3
3
|
module ActiveElement
|
4
4
|
# Processes all controller actions, verifies permissions, issues redirects, etc.
|
5
5
|
class ControllerAction
|
6
|
+
include Paintbrush
|
7
|
+
|
6
8
|
def initialize(controller)
|
7
9
|
@controller = controller
|
8
10
|
end
|
9
11
|
|
10
12
|
def process_action
|
11
|
-
Rails.logger.info("
|
13
|
+
Rails.logger.info("#{ActiveElement.log_tag} #{colorized_permissions_message}")
|
12
14
|
return if verified_permissions?
|
13
15
|
|
14
|
-
warn "
|
16
|
+
warn "#{log_tag} #{colorized_permissions_message}" if Rails.env.test?
|
15
17
|
return controller.redirect_to redirect_path if redirect_to_default_landing_page?
|
16
18
|
|
17
19
|
render_forbidden
|
@@ -22,6 +24,7 @@ module ActiveElement
|
|
22
24
|
attr_reader :controller
|
23
25
|
|
24
26
|
def verified_permissions?
|
27
|
+
return true unless controller.active_element.authorize?
|
25
28
|
return @verified_permissions if defined?(@verified_permissions)
|
26
29
|
|
27
30
|
(@verified_permissions = permissions_check.permitted?)
|
@@ -50,8 +53,8 @@ module ActiveElement
|
|
50
53
|
|
51
54
|
def permissions_check
|
52
55
|
@permissions_check ||= PermissionsCheck.new(
|
53
|
-
required: controller.class.
|
54
|
-
actual: controller.current_user&.permissions,
|
56
|
+
required: controller.class.active_element.permissions,
|
57
|
+
actual: controller.active_element.current_user&.permissions,
|
55
58
|
controller_path: controller.controller_path,
|
56
59
|
action_name: controller.action_name,
|
57
60
|
rails_component: rails_component
|
@@ -60,7 +63,7 @@ module ActiveElement
|
|
60
63
|
|
61
64
|
def routes
|
62
65
|
@routes ||= Routes.new(
|
63
|
-
permissions: controller.current_user
|
66
|
+
permissions: controller.active_element.current_user&.permissions,
|
64
67
|
rails_component: rails_component
|
65
68
|
)
|
66
69
|
end
|
@@ -71,7 +74,7 @@ module ActiveElement
|
|
71
74
|
else
|
72
75
|
(rails_component.environment == 'test' ? :yellow : :red)
|
73
76
|
end
|
74
|
-
|
77
|
+
paintbrush { public_send(color, permissions_check.message) }
|
75
78
|
end
|
76
79
|
|
77
80
|
def redirect_to_default_landing_page?
|
@@ -83,9 +86,5 @@ module ActiveElement
|
|
83
86
|
def rails_component
|
84
87
|
@rails_component ||= RailsComponent.new(::Rails)
|
85
88
|
end
|
86
|
-
|
87
|
-
def log_tag
|
88
|
-
ColorizedString.new('ActiveElement', color: :cyan).value
|
89
|
-
end
|
90
89
|
end
|
91
90
|
end
|