motor-admin 0.1.57 → 0.1.58

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/app/controllers/concerns/motor/current_ability.rb +21 -0
  4. data/app/controllers/concerns/motor/current_user_method.rb +8 -7
  5. data/app/controllers/motor/alerts_controller.rb +4 -4
  6. data/app/controllers/motor/api_base_controller.rb +1 -12
  7. data/app/controllers/motor/application_controller.rb +1 -0
  8. data/app/controllers/motor/audits_controller.rb +1 -1
  9. data/app/controllers/motor/configs_controller.rb +2 -2
  10. data/app/controllers/motor/dashboards_controller.rb +8 -4
  11. data/app/controllers/motor/data_controller.rb +9 -4
  12. data/app/controllers/motor/forms_controller.rb +4 -4
  13. data/app/controllers/motor/queries_controller.rb +4 -4
  14. data/app/controllers/motor/resources_controller.rb +2 -2
  15. data/app/controllers/motor/run_queries_controller.rb +12 -1
  16. data/app/controllers/motor/ui_controller.rb +1 -1
  17. data/app/views/motor/ui/show.html.erb +1 -1
  18. data/lib/motor.rb +1 -0
  19. data/lib/motor/admin.rb +8 -0
  20. data/lib/motor/api_query/build_json.rb +101 -47
  21. data/lib/motor/assets.rb +10 -1
  22. data/lib/motor/build_schema.rb +3 -1
  23. data/lib/motor/build_schema/active_storage_attachment_schema.rb +1 -0
  24. data/lib/motor/build_schema/apply_permissions.rb +50 -0
  25. data/lib/motor/build_schema/find_icon.rb +6 -1
  26. data/lib/motor/cancan_utils.rb +7 -0
  27. data/lib/motor/cancan_utils/ability_patch.rb +29 -0
  28. data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
  29. data/lib/motor/configs/build_ui_app_tag.rb +26 -16
  30. data/lib/motor/configs/load_from_cache.rb +20 -8
  31. data/lib/motor/queries/run_query.rb +7 -3
  32. data/lib/motor/version.rb +1 -1
  33. data/ui/dist/{main-dd83302a0e62f7cfdba6.css.gz → main-ba741735a93c9314bbfa.css.gz} +0 -0
  34. data/ui/dist/main-ba741735a93c9314bbfa.js.gz +0 -0
  35. data/ui/dist/manifest.json +5 -5
  36. metadata +11 -6
  37. data/ui/dist/main-dd83302a0e62f7cfdba6.js.gz +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd48d06a028ec31b8836ef75713286dc9622d1eb7322ad851e3f5b8c08ca8783
4
- data.tar.gz: b2f09fa05a3380f4da4af372d25597ede7081c1be533ce7ee5c047b43a5ee07e
3
+ metadata.gz: 98eed032de058a25fc5951c40afac9faeedd33ad66299ab381f15527e122aeeb
4
+ data.tar.gz: 4797ba6520bf3614ced8ea7aa800145f2ee7c35934820c5ae623dfe19c007a45
5
5
  SHA512:
6
- metadata.gz: 6eed979d27d7f266b986ef3433efc5863d6e7ae46cd446b48decb77fd8e24ff28250d9124d79e3c63bfe704930ab2e936bd048d3f4f350a1b5a6dfe4b4e26f5d
7
- data.tar.gz: d1fc8ac42009bd30f98373d6fe9f48ffc6cdc421be688e552dd031d54656115a979b6403d0dd3ac0de9ec1813f3b2c9860311cc87b0c88a438670651e45c8e87
6
+ metadata.gz: 7d966ae337a44f486a56f0dcb741d06787af84e67e4a4f5c5f750796b6a8b7979e9685ec9485854e34d994477ddcfdbc55882d9efc0dd26b356f8bbfe66bb0dd
7
+ data.tar.gz: 82e3f0b57ef8aa4902a36da9a8d94814f3d42f5b18cd73d70a95e59d69b5737d8652505bf3b6468629b51fef06c55e55b3ab03840838db8b1222edbc9ef9a3df
data/README.md CHANGED
@@ -32,6 +32,7 @@ $ rails motor:install && rake db:migrate
32
32
  * [Data visualization](#data-visualization)
33
33
  * [Dashboards](#dashboards)
34
34
  * [Email alerts](#email-alerts)
35
+ * [Authorization](#authorization)
35
36
  * [Intelligence search](#intelligence-search)
36
37
  * [Optimized for mobile](#optimized-for-mobile)
37
38
  * [Configurations sync between environments](#configurations-sync)
@@ -90,6 +91,9 @@ Sender address can be specified using `MOTOR_ALERTS_FROM_ADDRESS` environment va
90
91
 
91
92
  Intelligence search can be opened via the top right corner button or using <kbd>Cmd</kbd> + <kbd>P</kbd> shortcut.
92
93
 
94
+ ### Authorization
95
+
96
+ Motor Admin allows to set row-level and column-level permissions via [cancan](https://github.com/CanCanCommunity/cancancan) gem. Admin UI permissions should be defined in `app/models/motor/ability.rb` file in `Motor::Ability` class. See [Motor Admin guide](https://github.com/omohokcoj/motor-admin/blob/master/guides/defining_prmissions.md) and [CanCan documentation](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Defining-Abilities.md) to learn how to define user permissions.
93
97
 
94
98
  ### Optimized for Mobile
95
99
 
@@ -141,8 +145,6 @@ MOTOR_DEVELOPMENT=true rails s
141
145
 
142
146
  ## Comming Soon
143
147
 
144
- * User groups
145
- * Row-level permissions
146
148
  * Multiple databases
147
149
  * NoSQL data sources
148
150
  * Pro Bussines intelligence features
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module CurrentAbility
5
+ def current_ability
6
+ @current_ability ||=
7
+ if defined?(Motor::Ability) && current_user
8
+ klass = Motor::Ability.dup.tap do |k|
9
+ k.prepend(Motor::CancanUtils::AbilityPatch)
10
+ end
11
+
12
+ params = [current_user]
13
+ params << request if Motor::Ability.instance_method(:initialize).arity == 2
14
+
15
+ klass.new(*params)
16
+ else
17
+ Motor::CancanUtils::CanManageAll.new
18
+ end
19
+ end
20
+ end
21
+ end
@@ -3,13 +3,14 @@
3
3
  module Motor
4
4
  module CurrentUserMethod
5
5
  def current_user
6
- if defined?(current_admin)
7
- current_admin
8
- elsif defined?(current_admin_user)
9
- current_admin_user
10
- elsif defined?(super)
11
- super
12
- end
6
+ @current_user ||=
7
+ if defined?(current_admin)
8
+ current_admin
9
+ elsif defined?(current_admin_user)
10
+ current_admin_user
11
+ elsif defined?(super)
12
+ super
13
+ end
13
14
  end
14
15
  end
15
16
  end
@@ -10,11 +10,11 @@ module Motor
10
10
  authorize_resource :alert, only: :create
11
11
 
12
12
  def index
13
- render json: { data: Motor::ApiQuery::BuildJson.call(@alerts.active, params) }
13
+ render json: { data: Motor::ApiQuery::BuildJson.call(@alerts.active, params, current_ability) }
14
14
  end
15
15
 
16
16
  def show
17
- render json: { data: Motor::ApiQuery::BuildJson.call(@alert, params) }
17
+ render json: { data: Motor::ApiQuery::BuildJson.call(@alert, params, current_ability) }
18
18
  end
19
19
 
20
20
  def create
@@ -25,7 +25,7 @@ module Motor
25
25
  Motor::Alerts::ScheduledAlertsCache.clear
26
26
  Motor::Configs::WriteToFile.call
27
27
 
28
- render json: { data: Motor::ApiQuery::BuildJson.call(@alert, params) }
28
+ render json: { data: Motor::ApiQuery::BuildJson.call(@alert, params, current_ability) }
29
29
  end
30
30
  rescue Motor::Alerts::Persistance::InvalidInterval
31
31
  invalid_interval_response
@@ -36,7 +36,7 @@ module Motor
36
36
  Motor::Alerts::ScheduledAlertsCache.clear
37
37
  Motor::Configs::WriteToFile.call
38
38
 
39
- render json: { data: Motor::ApiQuery::BuildJson.call(@alert, params) }
39
+ render json: { data: Motor::ApiQuery::BuildJson.call(@alert, params, current_ability) }
40
40
  rescue Motor::Alerts::Persistance::NameAlreadyExists
41
41
  name_already_exists_response
42
42
  rescue Motor::Alerts::Persistance::InvalidInterval
@@ -3,14 +3,7 @@
3
3
  module Motor
4
4
  class ApiBaseController < ActionController::API
5
5
  include Motor::CurrentUserMethod
6
-
7
- class CanCanAbilityManageAll
8
- include CanCan::Ability
9
-
10
- def initialize(_)
11
- can :manage, :all
12
- end
13
- end
6
+ include Motor::CurrentAbility
14
7
 
15
8
  unless Rails.env.test?
16
9
  rescue_from StandardError do |e|
@@ -19,9 +12,5 @@ module Motor
19
12
  render json: { errors: [e.message] }, status: :internal_server_error
20
13
  end
21
14
  end
22
-
23
- def current_ability
24
- CanCanAbilityManageAll.new(current_user)
25
- end
26
15
  end
27
16
  end
@@ -3,5 +3,6 @@
3
3
  module Motor
4
4
  class ApplicationController < ActionController::Base
5
5
  include Motor::CurrentUserMethod
6
+ include Motor::CurrentAbility
6
7
  end
7
8
  end
@@ -8,7 +8,7 @@ module Motor
8
8
  audits = Motor::ApiQuery.call(@audits, params)
9
9
 
10
10
  render json: {
11
- data: Motor::ApiQuery::BuildJson.call(audits, params),
11
+ data: Motor::ApiQuery::BuildJson.call(audits, params, current_ability),
12
12
  meta: Motor::ApiQuery::BuildMeta.call(audits, params)
13
13
  }
14
14
  end
@@ -7,7 +7,7 @@ module Motor
7
7
  load_and_authorize_resource
8
8
 
9
9
  def index
10
- render json: { data: Motor::ApiQuery::BuildJson.call(@configs, params) }
10
+ render json: { data: Motor::ApiQuery::BuildJson.call(@configs, params, current_ability) }
11
11
  end
12
12
 
13
13
  def create
@@ -19,7 +19,7 @@ module Motor
19
19
  @config.save!
20
20
  Motor::Configs::WriteToFile.call
21
21
 
22
- render json: { data: Motor::ApiQuery::BuildJson.call(@config, params) }
22
+ render json: { data: Motor::ApiQuery::BuildJson.call(@config, params, current_ability) }
23
23
  rescue ActiveRecord::RecordNotUnique
24
24
  retry
25
25
  end
@@ -10,11 +10,11 @@ module Motor
10
10
  authorize_resource :dashboard, only: :create
11
11
 
12
12
  def index
13
- render json: { data: Motor::ApiQuery::BuildJson.call(@dashboards.active, params) }
13
+ render json: { data: Motor::ApiQuery::BuildJson.call(@dashboards.active, params, current_ability) }
14
14
  end
15
15
 
16
16
  def show
17
- render json: { data: Motor::ApiQuery::BuildJson.call(@dashboard, params) }
17
+ render json: { data: Motor::ApiQuery::BuildJson.call(@dashboard, params, current_ability) }
18
18
  end
19
19
 
20
20
  def create
@@ -24,7 +24,7 @@ module Motor
24
24
  ApplicationRecord.transaction { @dashboard.save! }
25
25
  Motor::Configs::WriteToFile.call
26
26
 
27
- render json: { data: Motor::ApiQuery::BuildJson.call(@dashboard, params) }
27
+ render json: { data: Motor::ApiQuery::BuildJson.call(@dashboard, params, current_ability) }
28
28
  end
29
29
  rescue ActiveRecord::RecordNotUnique
30
30
  retry
@@ -34,7 +34,7 @@ module Motor
34
34
  Motor::Dashboards::Persistance.update_from_params!(@dashboard, dashboard_params)
35
35
  Motor::Configs::WriteToFile.call
36
36
 
37
- render json: { data: Motor::ApiQuery::BuildJson.call(@dashboard, params) }
37
+ render json: { data: Motor::ApiQuery::BuildJson.call(@dashboard, params, current_ability) }
38
38
  rescue Motor::Dashboards::Persistance::TitleAlreadyExists
39
39
  render json: { errors: [{ source: 'title', detail: 'Title already exists' }] }, status: :unprocessable_entity
40
40
  end
@@ -51,6 +51,10 @@ module Motor
51
51
 
52
52
  def build_dashboard
53
53
  @dashboard = Motor::Dashboards::Persistance.build_from_params(dashboard_params)
54
+
55
+ @dashboard.define_singleton_method(:tags) do
56
+ taggable_tags.map(&:tag)
57
+ end
54
58
  end
55
59
 
56
60
  def dashboard_params
@@ -11,19 +11,19 @@ module Motor
11
11
  @resources = Motor::ApiQuery.call(@resources, params)
12
12
 
13
13
  render json: {
14
- data: Motor::ApiQuery::BuildJson.call(@resources, params),
14
+ data: Motor::ApiQuery::BuildJson.call(@resources, params, current_ability),
15
15
  meta: Motor::ApiQuery::BuildMeta.call(@resources, params)
16
16
  }
17
17
  end
18
18
 
19
19
  def show
20
- render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
20
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params, current_ability) }
21
21
  end
22
22
 
23
23
  def create
24
24
  @resource.save!
25
25
 
26
- render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
26
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params, current_ability) }
27
27
  rescue ActiveRecord::RecordInvalid
28
28
  render json: { errors: @resource.errors }, status: :unprocessable_entity
29
29
  end
@@ -31,7 +31,7 @@ module Motor
31
31
  def update
32
32
  @resource.update!(resource_params)
33
33
 
34
- render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
34
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params, current_ability) }
35
35
  rescue ActiveRecord::RecordInvalid
36
36
  render json: { errors: @resource.errors }, status: :unprocessable_entity
37
37
  end
@@ -47,6 +47,11 @@ module Motor
47
47
  end
48
48
 
49
49
  def execute
50
+ resource_preferences = Motor::Resource.find_by(name: @resource.class.name.underscore).preferences
51
+ resource_action = resource_preferences[:actions].find { |a| a[:preferences][:method_name] == params[:method] }
52
+
53
+ authorize!(resource_action[:name].to_sym, @resource)
54
+
50
55
  @resource.public_send(params[:method].to_sym)
51
56
 
52
57
  head :ok
@@ -10,11 +10,11 @@ module Motor
10
10
  authorize_resource :form, only: :create
11
11
 
12
12
  def index
13
- render json: { data: Motor::ApiQuery::BuildJson.call(@forms.active, params) }
13
+ render json: { data: Motor::ApiQuery::BuildJson.call(@forms.active, params, current_ability) }
14
14
  end
15
15
 
16
16
  def show
17
- render json: { data: Motor::ApiQuery::BuildJson.call(@form, params) }
17
+ render json: { data: Motor::ApiQuery::BuildJson.call(@form, params, current_ability) }
18
18
  end
19
19
 
20
20
  def create
@@ -24,7 +24,7 @@ module Motor
24
24
  ApplicationRecord.transaction { @form.save! }
25
25
  Motor::Configs::WriteToFile.call
26
26
 
27
- render json: { data: Motor::ApiQuery::BuildJson.call(@form, params) }
27
+ render json: { data: Motor::ApiQuery::BuildJson.call(@form, params, current_ability) }
28
28
  end
29
29
  rescue ActiveRecord::RecordNotUnique
30
30
  retry
@@ -34,7 +34,7 @@ module Motor
34
34
  Motor::Forms::Persistance.update_from_params!(@form, form_params)
35
35
  Motor::Configs::WriteToFile.call
36
36
 
37
- render json: { data: Motor::ApiQuery::BuildJson.call(@form, params) }
37
+ render json: { data: Motor::ApiQuery::BuildJson.call(@form, params, current_ability) }
38
38
  rescue Motor::Forms::Persistance::NameAlreadyExists
39
39
  render json: { errors: [{ source: 'name', detail: 'Name already exists' }] }, status: :unprocessable_entity
40
40
  end
@@ -10,11 +10,11 @@ module Motor
10
10
  authorize_resource :query, only: :create
11
11
 
12
12
  def index
13
- render json: { data: Motor::ApiQuery::BuildJson.call(@queries.active, params) }
13
+ render json: { data: Motor::ApiQuery::BuildJson.call(@queries.active, params, current_ability) }
14
14
  end
15
15
 
16
16
  def show
17
- render json: { data: Motor::ApiQuery::BuildJson.call(@query, params) }
17
+ render json: { data: Motor::ApiQuery::BuildJson.call(@query, params, current_ability) }
18
18
  end
19
19
 
20
20
  def create
@@ -24,7 +24,7 @@ module Motor
24
24
  ApplicationRecord.transaction { @query.save! }
25
25
  Motor::Configs::WriteToFile.call
26
26
 
27
- render json: { data: Motor::ApiQuery::BuildJson.call(@query, params) }
27
+ render json: { data: Motor::ApiQuery::BuildJson.call(@query, params, current_ability) }
28
28
  end
29
29
  rescue ActiveRecord::RecordNotUnique
30
30
  retry
@@ -34,7 +34,7 @@ module Motor
34
34
  Motor::Queries::Persistance.update_from_params!(@query, query_params)
35
35
  Motor::Configs::WriteToFile.call
36
36
 
37
- render json: { data: Motor::ApiQuery::BuildJson.call(@query, params) }
37
+ render json: { data: Motor::ApiQuery::BuildJson.call(@query, params, current_ability) }
38
38
  rescue Motor::Queries::Persistance::NameAlreadyExists
39
39
  render json: { errors: [{ source: 'name', detail: 'Name already exists' }] }, status: :unprocessable_entity
40
40
  end
@@ -7,14 +7,14 @@ module Motor
7
7
  load_and_authorize_resource
8
8
 
9
9
  def index
10
- render json: { data: Motor::ApiQuery::BuildJson.call(@resources, params) }
10
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resources, params, current_ability) }
11
11
  end
12
12
 
13
13
  def create
14
14
  Motor::BuildSchema::PersistResourceConfigs.call(@resource)
15
15
  Motor::Configs::WriteToFile.call
16
16
 
17
- render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
17
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params, current_ability) }
18
18
  end
19
19
 
20
20
  private
@@ -20,7 +20,8 @@ module Motor
20
20
  private
21
21
 
22
22
  def render_result
23
- query_result = Queries::RunQuery.call(@query, variables_hash: params[:variables])
23
+ variables = params.fetch(:variables, {}).merge(current_user_variables)
24
+ query_result = Queries::RunQuery.call(@query, variables_hash: variables)
24
25
 
25
26
  if query_result.error
26
27
  render json: { errors: [{ detail: query_result.error }] }, status: :unprocessable_entity
@@ -29,6 +30,16 @@ module Motor
29
30
  end
30
31
  end
31
32
 
33
+ def current_user_variables
34
+ return {} unless current_user
35
+
36
+ current_user
37
+ .attributes
38
+ .slice('id', 'email')
39
+ .transform_keys { |key| "current_user_#{key}" }
40
+ .compact
41
+ end
42
+
32
43
  def query_result_hash(query_result)
33
44
  {
34
45
  data: query_result.data,
@@ -4,7 +4,7 @@ module Motor
4
4
  class UiController < ApplicationController
5
5
  layout 'motor/application'
6
6
 
7
- helper_method :current_user
7
+ helper_method :current_user, :current_ability
8
8
 
9
9
  def index
10
10
  render_ui
@@ -1 +1 @@
1
- <%= raw(Motor::Configs::BuildUiAppTag.call(current_user)) %>
1
+ <%= raw(Motor::Configs::BuildUiAppTag.call(current_user, current_ability)) %>
data/lib/motor.rb CHANGED
@@ -53,6 +53,7 @@ require 'motor/version'
53
53
  require 'motor/admin'
54
54
  require 'motor/assets'
55
55
  require 'motor/active_record_utils'
56
+ require 'motor/cancan_utils'
56
57
  require 'motor/build_schema'
57
58
  require 'motor/api_query'
58
59
  require 'motor/tags'
data/lib/motor/admin.rb CHANGED
@@ -69,6 +69,14 @@ module Motor
69
69
  config.after_initialize do
70
70
  next unless defined?(ActiveStorage::Engine)
71
71
 
72
+ ActiveSupport.on_load(:active_storage_attachment) do
73
+ ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
74
+ end
75
+
76
+ ActiveSupport.on_load(:active_storage_blob) do
77
+ ActiveStorage::Blob.singleton_class.prepend(Motor::ActiveRecordUtils::ActiveStorageBlobPatch)
78
+ end
79
+
72
80
  ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
73
81
  ActiveStorage::Blob.singleton_class.prepend(Motor::ActiveRecordUtils::ActiveStorageBlobPatch)
74
82
  end
@@ -5,83 +5,90 @@ module Motor
5
5
  module BuildJson
6
6
  module_function
7
7
 
8
- def call(rel, params)
9
- rel = rel.none if params.dig(:page, :limit).yield_self { |limit| limit.present? && limit.to_i.zero? }
10
-
8
+ # @param rel [ActiveRecord::Base, ActiveRecord::Relation]
9
+ # @param params [Hash]
10
+ # @param current_ability [CanCan::Ability]
11
+ # @return [Hash]
12
+ def call(rel, params, current_ability = Motor::CancanUtils::CanManageAll.new)
13
+ rel = rel.none if limit_zero_params?(params)
11
14
  rel = rel.preload_associations_lazily if rel.is_a?(ActiveRecord::Relation)
12
15
 
13
- json_params = {}.with_indifferent_access
14
-
15
- assign_include_params(json_params, rel, params)
16
- assign_fields_params(json_params, rel, params)
17
-
18
- rel.as_json(json_params)
19
- end
16
+ model = rel.is_a?(ActiveRecord::Relation) ? rel.klass : rel.class
20
17
 
21
- def assign_include_params(json_params, _rel, api_params)
22
- return if api_params['include'].blank?
18
+ include_hash = build_include_hash(params['include'])
19
+ models_index = build_models_index(model, include_hash)
23
20
 
24
- include_params = api_params['include']
21
+ json_params = normalize_include_params(include_hash)
25
22
 
26
- if include_params.is_a?(String)
27
- include_params =
28
- include_params.split(',').reduce({}) do |accumulator, path|
29
- hash = {}
23
+ assign_fields_params!(json_params, model, params, current_ability, models_index)
30
24
 
31
- path.split('.').reduce(hash) do |acc, part|
32
- acc_hash = {}
33
-
34
- acc[part] = acc_hash
25
+ rel.as_json(json_params.with_indifferent_access)
26
+ end
35
27
 
36
- acc_hash
37
- end
28
+ # @param include_params [Hash]
29
+ # @return [Hash]
30
+ def build_include_hash(include_params)
31
+ return {} if include_params.blank?
38
32
 
39
- accumulator.deep_merge(hash)
40
- end
33
+ if include_params.is_a?(String)
34
+ build_hash_from_string_path(include_params)
35
+ else
36
+ include_params
41
37
  end
42
-
43
- json_params.deep_merge!(normalize_include_params(include_params))
44
38
  end
45
39
 
46
- def assign_fields_params(json_params, rel, params)
40
+ # @param json_params [Hash]
41
+ # @param model [Class<ActiveRecord::Base>]
42
+ # @param params [Hash]
43
+ # @param current_ability [CanCan::Ability]
44
+ # @param models_index [Hash]
45
+ # @return [void]
46
+ def assign_fields_params!(json_params, model, params, current_ability, models_index)
47
47
  return if params[:fields].blank?
48
48
 
49
- model = rel.is_a?(ActiveRecord::Relation) ? rel.klass : rel.class
50
-
51
49
  params[:fields].each do |key, fields|
52
- fields = fields.split(',') if fields.is_a?(String)
53
-
54
- merge_fields_params!(json_params, key, fields, model)
55
- end
56
- end
50
+ fields_model = models_index[key]
57
51
 
58
- def merge_fields_params!(json_params, key, fields, model)
59
- model_name = model.name.underscore
52
+ next unless model
60
53
 
61
- if key == model_name || model_name.split('/').last == key
62
- json_params.merge!(build_fields_hash(model, fields))
63
- else
64
- hash = find_key_in_params(json_params, key)
54
+ fields = fields.split(',') if fields.is_a?(String)
65
55
 
66
- fields_hash = build_fields_hash(model.reflections[key]&.klass, fields)
56
+ fields_hash = fields_model == model ? json_params : find_key_in_params(json_params, key)
67
57
 
68
- hash.merge!(fields_hash)
58
+ fields_hash.merge!(build_fields_hash(fields_model, fields, current_ability))
69
59
  end
70
60
  end
71
61
 
72
- def build_fields_hash(model, fields)
73
- columns = model ? model.columns.map(&:name) : []
62
+ # @param model [Class<ActiveRecord::Base>]
63
+ # @param fields [Hash]
64
+ # @param current_ability [CanCan::Ability]
65
+ # @return [Hash]
66
+ def build_fields_hash(model, fields, current_ability)
67
+ return { 'methods' => fields } unless model
68
+
69
+ column_names = model.column_names.map(&:to_sym)
70
+ instance_methods = model.instance_methods
71
+ permitted_attributes = current_ability.permitted_attributes(:read, model)
72
+ is_permitted_all = column_names == permitted_attributes
73
+
74
74
  fields_hash = { 'only' => [], 'methods' => [] }
75
75
 
76
76
  fields.each_with_object(fields_hash) do |field, acc|
77
- if field.in?(columns)
77
+ field_symbol = field.to_sym
78
+
79
+ next if !is_permitted_all && permitted_attributes.exclude?(field_symbol)
80
+
81
+ if column_names.include?(field_symbol)
78
82
  acc['only'] << field
79
- elsif model.nil? || model.instance_methods.include?(field.to_sym)
83
+ elsif instance_methods.include?(field_symbol)
80
84
  acc['methods'] << field
81
85
  end
82
86
  end
83
87
  end
84
88
 
89
+ # @param params [Hash]
90
+ # @param key [String]
91
+ # @return [Hash]
85
92
  def find_key_in_params(params, key)
86
93
  params = params['include']
87
94
 
@@ -93,6 +100,8 @@ module Motor
93
100
  end
94
101
  end
95
102
 
103
+ # @param params [Hash]
104
+ # @return [Hash]
96
105
  def normalize_include_params(params)
97
106
  case params
98
107
  when Array
@@ -112,6 +121,51 @@ module Motor
112
121
  raise ArgumentError, "Wrong include param type #{params.class}"
113
122
  end
114
123
  end
124
+
125
+ # @param model [Class<ActiveRecord::Base>]
126
+ # @param includes_hash [Hash]
127
+ # @return [Hash]
128
+ def build_models_index(model, includes_hash)
129
+ default_index = {
130
+ model.name.underscore => model,
131
+ model.name.underscore.split('/').last => model
132
+ }
133
+
134
+ includes_hash.reduce(default_index) do |acc, (key, value)|
135
+ reflection = model.reflections[key]
136
+
137
+ next acc unless reflection
138
+ next acc if reflection.polymorphic?
139
+
140
+ acc[key] = reflection.klass
141
+
142
+ acc.merge(build_models_index(reflection.klass, value))
143
+ end
144
+ end
145
+
146
+ # @param string_path [String]
147
+ # @return [Hash]
148
+ def build_hash_from_string_path(string_path)
149
+ string_path.split(',').reduce({}) do |accumulator, path|
150
+ hash = {}
151
+
152
+ path.split('.').reduce(hash) do |acc, part|
153
+ acc_hash = {}
154
+
155
+ acc[part] = acc_hash
156
+
157
+ acc_hash
158
+ end
159
+
160
+ accumulator.deep_merge(hash)
161
+ end
162
+ end
163
+
164
+ # @param params [Hash]
165
+ # @return [Boolean]
166
+ def limit_zero_params?(params)
167
+ params.dig(:page, :limit).yield_self { |limit| limit.present? && limit.to_i.zero? }
168
+ end
115
169
  end
116
170
  end
117
171
  end
data/lib/motor/assets.rb CHANGED
@@ -8,10 +8,19 @@ module Motor
8
8
  MANIFEST_PATH = ASSETS_PATH.join('manifest.json')
9
9
  DEV_SERVER_URL = 'http://localhost:9090/'
10
10
 
11
+ CACHE_STORE =
12
+ if Rails.env.production?
13
+ ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
14
+ else
15
+ ActiveSupport::Cache::NullStore.new
16
+ end
17
+
11
18
  module_function
12
19
 
13
20
  def manifest
14
- JSON.parse(MANIFEST_PATH.read)
21
+ CACHE_STORE.fetch('manifest') do
22
+ JSON.parse(MANIFEST_PATH.read)
23
+ end
15
24
  end
16
25
 
17
26
  def icons
@@ -58,9 +58,10 @@ module Motor
58
58
 
59
59
  module_function
60
60
 
61
- def call(cache_keys = {})
61
+ def call(cache_keys = {}, current_ability = nil)
62
62
  schema = LoadFromRails.call
63
63
  schema = MergeSchemaConfigs.call(schema, cache_keys)
64
+ schema = ApplyPermissions.call(schema, current_ability) if current_ability
64
65
 
65
66
  ReorderSchema.call(schema, cache_keys)
66
67
  end
@@ -75,4 +76,5 @@ require_relative './build_schema/find_icon'
75
76
  require_relative './build_schema/persist_resource_configs'
76
77
  require_relative './build_schema/reorder_schema'
77
78
  require_relative './build_schema/merge_schema_configs'
79
+ require_relative './build_schema/apply_permissions'
78
80
  require_relative './build_schema/utils'
@@ -5,6 +5,7 @@ module Motor
5
5
  ACTIVE_STORAGE_ATTACHMENT_SCHEMA = {
6
6
  name: 'active_storage/attachment',
7
7
  slug: 'active_storage__attachments',
8
+ class_name: 'ActiveStorage::Attachment',
8
9
  table_name: 'active_storage_attachments',
9
10
  primary_key: 'id',
10
11
  display_name: 'Attachments',
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module ApplyPermissions
6
+ module_function
7
+
8
+ def call(schema, ability)
9
+ schema.map do |model|
10
+ klass = model[:class_name].constantize
11
+
12
+ next unless ability.can?(:read, klass)
13
+
14
+ model[:associations] = filter_associations(model[:associations], ability)
15
+ model[:columns] = filter_columns(klass, model[:columns], ability)
16
+ model[:actions] = filter_actions(klass, model[:actions], ability)
17
+
18
+ model
19
+ end.compact
20
+ end
21
+
22
+ def filter_associations(associations, ability)
23
+ associations.select do |assoc|
24
+ ability.can?(:read, assoc[:model_name].classify.constantize)
25
+ end
26
+ end
27
+
28
+ def filter_columns(model, columns, ability)
29
+ columns.map do |column|
30
+ next unless ability.can?(:read, model, column[:name])
31
+
32
+ next if column.dig(:reference, :model_name).present? &&
33
+ !ability.can?(:read, column[:reference][:model_name].classify.constantize)
34
+
35
+ unless ability.can?(:update, model, column[:name])
36
+ column = column.merge(access_type: BuildSchema::ColumnAccessTypes::READ_ONLY)
37
+ end
38
+
39
+ column
40
+ end.compact
41
+ end
42
+
43
+ def filter_actions(model, actions, ability)
44
+ actions.select do |action|
45
+ ability.can?(action[:name].to_sym, model)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -35,6 +35,7 @@ module Motor
35
35
  'token' => 'key',
36
36
  'secret' => 'lock',
37
37
  'automation' => 'manual-gearbox',
38
+ 'workflow' => 'manual-gearbox',
38
39
  'relationship' => 'hierarchy',
39
40
  'person' => 'user',
40
41
  'people' => 'users',
@@ -96,6 +97,9 @@ module Motor
96
97
  'page' => 'brand-pagekit',
97
98
  'date' => 'calendar-event',
98
99
  'customer' => 'users',
100
+ 'client' => 'users',
101
+ 'ticket' => 'ticket',
102
+ 'event' => 'event',
99
103
  'contact' => 'users',
100
104
  'member' => 'users',
101
105
  'admin' => 'user-check',
@@ -107,7 +111,8 @@ module Motor
107
111
  'product' => 'building-store',
108
112
  'html' => 'code',
109
113
  'stripe' => 'brand-stripe',
110
- 'email' => 'mail'
114
+ 'email' => 'mail',
115
+ 'status' => 'hash'
111
116
  }.freeze
112
117
 
113
118
  DEFAULT_ICON = 'database'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CancanUtils
4
+ end
5
+
6
+ require_relative './cancan_utils/ability_patch'
7
+ require_relative './cancan_utils/can_manage_all'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module CancanUtils
5
+ module AbilityPatch
6
+ def serialized_rules
7
+ @rules.map do |rule|
8
+ {
9
+ base_behavior: rule.base_behavior,
10
+ actions: expand_actions(rule.actions),
11
+ subjects: rule.subjects.map(&:to_s),
12
+ attributes: rule.attributes,
13
+ conditions: rule.conditions.as_json
14
+ }
15
+ end
16
+ end
17
+
18
+ def rules_hash
19
+ serialized_rules.hash
20
+ end
21
+
22
+ private
23
+
24
+ def default_alias_actions
25
+ super.merge(destroy: %i[remove delete])
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module CancanUtils
5
+ class CanManageAll
6
+ include CanCan::Ability
7
+ prepend CancanUtils::AbilityPatch
8
+
9
+ def initialize(_ = nil)
10
+ can :manage, :all
11
+ end
12
+ end
13
+ end
14
+ end
@@ -12,57 +12,67 @@ module Motor
12
12
 
13
13
  module_function
14
14
 
15
- def call(current_user = nil)
15
+ def call(current_user = nil, current_ability = nil)
16
16
  cache_keys = LoadFromCache.load_cache_keys
17
17
 
18
18
  CACHE_STORE.fetch("#{cache_keys.hash}#{current_user&.id}") do
19
19
  CACHE_STORE.clear
20
20
 
21
- Motor::ApplicationController.helpers.tag.div('', id: 'app', data: build_data(cache_keys, current_user))
21
+ data = build_data(cache_keys, current_user, current_ability)
22
+ Motor::ApplicationController.helpers.tag.div('', id: 'app', data: data)
22
23
  end
23
24
  end
24
25
 
25
26
  # @return [Hash]
26
- def build_data(cache_keys = {}, current_user = nil)
27
+ def build_data(cache_keys = {}, current_user = nil, current_ability = nil)
27
28
  {
28
29
  current_user: current_user&.as_json(only: %i[id email]),
30
+ current_rules: current_ability.serialized_rules,
29
31
  audits_count: Motor::Audit.count,
30
32
  base_path: Motor::Admin.routes.url_helpers.motor_path,
31
- schema: Motor::BuildSchema.call(cache_keys),
33
+ schema: Motor::BuildSchema.call(cache_keys, current_ability),
32
34
  header_links: header_links_data_hash(cache_keys[:configs]),
33
- queries: queries_data_hash(cache_keys[:queries]),
34
- dashboards: dashboards_data_hash(cache_keys[:dashboards]),
35
- alerts: alerts_data_hash(cache_keys[:alerts]),
36
- forms: forms_data_hash(cache_keys[:forms])
35
+ queries: queries_data_hash(build_cache_key(cache_keys, :queries, current_user, current_ability),
36
+ current_ability),
37
+ dashboards: dashboards_data_hash(build_cache_key(cache_keys, :dashboards, current_user, current_ability),
38
+ current_ability),
39
+ alerts: alerts_data_hash(build_cache_key(cache_keys, :alerts, current_user, current_ability),
40
+ current_ability),
41
+ forms: forms_data_hash(build_cache_key(cache_keys, :forms, current_user, current_ability), current_ability)
37
42
  }
38
43
  end
39
44
 
45
+ # @return [String]
46
+ def build_cache_key(cache_keys, key, current_user, current_ability)
47
+ "#{cache_keys[key].hash}#{current_user&.id}#{current_ability&.rules_hash}"
48
+ end
49
+
40
50
  def header_links_data_hash(cache_key = nil)
41
51
  configs = Motor::Configs::LoadFromCache.load_configs(cache_key: cache_key)
42
52
 
43
53
  configs.find { |c| c.key == 'header.links' }&.value || []
44
54
  end
45
55
 
46
- def queries_data_hash(cache_key = nil)
47
- Motor::Configs::LoadFromCache.load_queries(cache_key: cache_key)
56
+ def queries_data_hash(cache_key = nil, current_ability = nil)
57
+ Motor::Configs::LoadFromCache.load_queries(cache_key: cache_key, current_ability: current_ability)
48
58
  .as_json(only: %i[id name updated_at],
49
59
  include: { tags: { only: %i[id name] } })
50
60
  end
51
61
 
52
- def dashboards_data_hash(cache_key = nil)
53
- Motor::Configs::LoadFromCache.load_dashboards(cache_key: cache_key)
62
+ def dashboards_data_hash(cache_key = nil, current_ability = nil)
63
+ Motor::Configs::LoadFromCache.load_dashboards(cache_key: cache_key, current_ability: current_ability)
54
64
  .as_json(only: %i[id title updated_at],
55
65
  include: { tags: { only: %i[id name] } })
56
66
  end
57
67
 
58
- def alerts_data_hash(cache_key = nil)
59
- Motor::Configs::LoadFromCache.load_alerts(cache_key: cache_key)
68
+ def alerts_data_hash(cache_key = nil, current_ability = nil)
69
+ Motor::Configs::LoadFromCache.load_alerts(cache_key: cache_key, current_ability: current_ability)
60
70
  .as_json(only: %i[id name is_enabled updated_at],
61
71
  include: { tags: { only: %i[id name] } })
62
72
  end
63
73
 
64
- def forms_data_hash(cache_key = nil)
65
- Motor::Configs::LoadFromCache.load_forms(cache_key: cache_key)
74
+ def forms_data_hash(cache_key = nil, current_ability = nil)
75
+ Motor::Configs::LoadFromCache.load_forms(cache_key: cache_key, current_ability: current_ability)
66
76
  .as_json(only: %i[id name updated_at],
67
77
  include: { tags: { only: %i[id name] } })
68
78
  end
@@ -32,27 +32,39 @@ module Motor
32
32
  end
33
33
  end
34
34
 
35
- def load_queries(cache_key: nil)
35
+ def load_queries(cache_key: nil, current_ability: nil)
36
36
  maybe_fetch_from_cache('queries', cache_key) do
37
- Motor::Query.all.active.preload(:tags).load
37
+ rel = Motor::Query.all.active.preload(:tags)
38
+ rel = rel.accessible_by(current_ability) if current_ability
39
+
40
+ rel.load
38
41
  end
39
42
  end
40
43
 
41
- def load_dashboards(cache_key: nil)
44
+ def load_dashboards(cache_key: nil, current_ability: nil)
42
45
  maybe_fetch_from_cache('dashboards', cache_key) do
43
- Motor::Dashboard.all.active.preload(:tags).load
46
+ rel = Motor::Dashboard.all.active.preload(:tags)
47
+ rel = rel.accessible_by(current_ability) if current_ability
48
+
49
+ rel.load
44
50
  end
45
51
  end
46
52
 
47
- def load_alerts(cache_key: nil)
53
+ def load_alerts(cache_key: nil, current_ability: nil)
48
54
  maybe_fetch_from_cache('alerts', cache_key) do
49
- Motor::Alert.all.active.preload(:tags).load
55
+ rel = Motor::Alert.all.active.preload(:tags)
56
+ rel = rel.accessible_by(current_ability) if current_ability
57
+
58
+ rel.load
50
59
  end
51
60
  end
52
61
 
53
- def load_forms(cache_key: nil)
62
+ def load_forms(cache_key: nil, current_ability: nil)
54
63
  maybe_fetch_from_cache('forms', cache_key) do
55
- Motor::Form.all.active.preload(:tags).load
64
+ rel = Motor::Form.all.active.preload(:tags)
65
+ rel = rel.accessible_by(current_ability) if current_ability
66
+
67
+ rel.load
56
68
  end
57
69
  end
58
70
 
@@ -16,6 +16,8 @@ module Motor
16
16
 
17
17
  PG_ERROR_REGEXP = /\APG.+ERROR:/.freeze
18
18
 
19
+ RESERVED_VARIABLES = %w[current_user_id current_user_email].freeze
20
+
19
21
  module_function
20
22
 
21
23
  # @param query [Motor::Query]
@@ -110,11 +112,13 @@ module Motor
110
112
  end
111
113
 
112
114
  # @param variable_configs [Array<Hash>]
113
- # @param variable_hash [Hash]
115
+ # @param variables_hash [Hash]
114
116
  # @return [Hash]
115
117
  def merge_variable_default_values(variable_configs, variables_hash)
116
- variable_configs.each_with_object({}) do |variable, acc|
117
- acc[variable[:name]] = variables_hash[variable[:name]] || variable[:default_value]
118
+ variable_configs.each_with_object(variables_hash.slice(*RESERVED_VARIABLES)) do |variable, acc|
119
+ next if RESERVED_VARIABLES.include?(variable[:name])
120
+
121
+ acc[variable[:name]] ||= variables_hash[variable[:name]] || variable[:default_value]
118
122
  end
119
123
  end
120
124
  end
data/lib/motor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- VERSION = '0.1.57'
4
+ VERSION = '0.1.58'
5
5
  end
@@ -2068,11 +2068,11 @@
2068
2068
  "mail-opened.svg": "icons/mail-opened.svg",
2069
2069
  "mail.svg": "icons/mail.svg",
2070
2070
  "mailbox.svg": "icons/mailbox.svg",
2071
- "main-dd83302a0e62f7cfdba6.css.gz": "main-dd83302a0e62f7cfdba6.css.gz",
2072
- "main-dd83302a0e62f7cfdba6.js.LICENSE.txt": "main-dd83302a0e62f7cfdba6.js.LICENSE.txt",
2073
- "main-dd83302a0e62f7cfdba6.js.gz": "main-dd83302a0e62f7cfdba6.js.gz",
2074
- "main.css": "main-dd83302a0e62f7cfdba6.css",
2075
- "main.js": "main-dd83302a0e62f7cfdba6.js",
2071
+ "main-ba741735a93c9314bbfa.css.gz": "main-ba741735a93c9314bbfa.css.gz",
2072
+ "main-ba741735a93c9314bbfa.js.LICENSE.txt": "main-ba741735a93c9314bbfa.js.LICENSE.txt",
2073
+ "main-ba741735a93c9314bbfa.js.gz": "main-ba741735a93c9314bbfa.js.gz",
2074
+ "main.css": "main-ba741735a93c9314bbfa.css",
2075
+ "main.js": "main-ba741735a93c9314bbfa.js",
2076
2076
  "man.svg": "icons/man.svg",
2077
2077
  "manual-gearbox.svg": "icons/manual-gearbox.svg",
2078
2078
  "map-2.svg": "icons/map-2.svg",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: motor-admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.57
4
+ version: 0.1.58
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pete Matsyburka
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-08 00:00:00.000000000 Z
11
+ date: 2021-06-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord-filter
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '4.9'
47
+ version: '5.0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '4.9'
54
+ version: '5.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: cancancan
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -121,6 +121,7 @@ files:
121
121
  - LICENSE
122
122
  - README.md
123
123
  - Rakefile
124
+ - app/controllers/concerns/motor/current_ability.rb
124
125
  - app/controllers/concerns/motor/current_user_method.rb
125
126
  - app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb
126
127
  - app/controllers/concerns/motor/wrap_io_params.rb
@@ -191,6 +192,7 @@ files:
191
192
  - lib/motor/build_schema.rb
192
193
  - lib/motor/build_schema/active_storage_attachment_schema.rb
193
194
  - lib/motor/build_schema/adjust_devise_model_schema.rb
195
+ - lib/motor/build_schema/apply_permissions.rb
194
196
  - lib/motor/build_schema/find_display_column.rb
195
197
  - lib/motor/build_schema/find_icon.rb
196
198
  - lib/motor/build_schema/load_from_rails.rb
@@ -198,6 +200,9 @@ files:
198
200
  - lib/motor/build_schema/persist_resource_configs.rb
199
201
  - lib/motor/build_schema/reorder_schema.rb
200
202
  - lib/motor/build_schema/utils.rb
203
+ - lib/motor/cancan_utils.rb
204
+ - lib/motor/cancan_utils/ability_patch.rb
205
+ - lib/motor/cancan_utils/can_manage_all.rb
201
206
  - lib/motor/configs.rb
202
207
  - lib/motor/configs/build_configs_hash.rb
203
208
  - lib/motor/configs/build_ui_app_tag.rb
@@ -1485,8 +1490,8 @@ files:
1485
1490
  - ui/dist/icons/zoom-money.svg.gz
1486
1491
  - ui/dist/icons/zoom-out.svg.gz
1487
1492
  - ui/dist/icons/zoom-question.svg.gz
1488
- - ui/dist/main-dd83302a0e62f7cfdba6.css.gz
1489
- - ui/dist/main-dd83302a0e62f7cfdba6.js.gz
1493
+ - ui/dist/main-ba741735a93c9314bbfa.css.gz
1494
+ - ui/dist/main-ba741735a93c9314bbfa.js.gz
1490
1495
  - ui/dist/manifest.json
1491
1496
  homepage:
1492
1497
  licenses: