super_admin 0.2.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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +216 -0
  3. data/Rakefile +30 -0
  4. data/app/assets/stylesheets/super_admin/application.css +15 -0
  5. data/app/assets/stylesheets/super_admin/tailwind.css +1 -0
  6. data/app/assets/stylesheets/super_admin/tailwind.source.css +25 -0
  7. data/app/controllers/super_admin/application_controller.rb +89 -0
  8. data/app/controllers/super_admin/associations_controller.rb +136 -0
  9. data/app/controllers/super_admin/audit_logs_controller.rb +39 -0
  10. data/app/controllers/super_admin/base_controller.rb +133 -0
  11. data/app/controllers/super_admin/dashboard_controller.rb +29 -0
  12. data/app/controllers/super_admin/exports_controller.rb +109 -0
  13. data/app/controllers/super_admin/resources_controller.rb +201 -0
  14. data/app/dashboards/super_admin/base_dashboard.rb +200 -0
  15. data/app/errors/super_admin/configuration_error.rb +6 -0
  16. data/app/helpers/super_admin/application_helper.rb +84 -0
  17. data/app/helpers/super_admin/exports_helper.rb +16 -0
  18. data/app/helpers/super_admin/resources_helper.rb +204 -0
  19. data/app/helpers/super_admin/route_helper.rb +7 -0
  20. data/app/javascript/super_admin/application.js +263 -0
  21. data/app/jobs/super_admin/application_job.rb +4 -0
  22. data/app/jobs/super_admin/generate_super_admin_csv_export_job.rb +100 -0
  23. data/app/mailers/super_admin/application_mailer.rb +6 -0
  24. data/app/models/super_admin/application_record.rb +5 -0
  25. data/app/models/super_admin/audit_log.rb +35 -0
  26. data/app/models/super_admin/csv_export.rb +67 -0
  27. data/app/services/super_admin/auditing.rb +74 -0
  28. data/app/services/super_admin/authorization.rb +113 -0
  29. data/app/services/super_admin/authorization_adapters/base_adapter.rb +100 -0
  30. data/app/services/super_admin/authorization_adapters/default_adapter.rb +77 -0
  31. data/app/services/super_admin/authorization_adapters/proc_adapter.rb +65 -0
  32. data/app/services/super_admin/authorization_adapters/pundit_adapter.rb +81 -0
  33. data/app/services/super_admin/csv_export_creator.rb +45 -0
  34. data/app/services/super_admin/dashboard_registry.rb +90 -0
  35. data/app/services/super_admin/dashboard_resolver.rb +100 -0
  36. data/app/services/super_admin/filter_builder.rb +185 -0
  37. data/app/services/super_admin/form_builder.rb +59 -0
  38. data/app/services/super_admin/form_fields/array_field.rb +35 -0
  39. data/app/services/super_admin/form_fields/association_field.rb +146 -0
  40. data/app/services/super_admin/form_fields/base_field.rb +53 -0
  41. data/app/services/super_admin/form_fields/boolean_field.rb +29 -0
  42. data/app/services/super_admin/form_fields/date_field.rb +15 -0
  43. data/app/services/super_admin/form_fields/date_time_field.rb +15 -0
  44. data/app/services/super_admin/form_fields/enum_field.rb +27 -0
  45. data/app/services/super_admin/form_fields/factory.rb +102 -0
  46. data/app/services/super_admin/form_fields/nested_field.rb +120 -0
  47. data/app/services/super_admin/form_fields/number_field.rb +29 -0
  48. data/app/services/super_admin/form_fields/text_area_field.rb +19 -0
  49. data/app/services/super_admin/model_inspector.rb +182 -0
  50. data/app/services/super_admin/queries/base_query.rb +45 -0
  51. data/app/services/super_admin/queries/filter_query.rb +188 -0
  52. data/app/services/super_admin/queries/resource_scope_query.rb +74 -0
  53. data/app/services/super_admin/queries/search_query.rb +146 -0
  54. data/app/services/super_admin/queries/sort_query.rb +41 -0
  55. data/app/services/super_admin/resource_configuration.rb +63 -0
  56. data/app/services/super_admin/resource_exporter.rb +78 -0
  57. data/app/services/super_admin/resource_query.rb +40 -0
  58. data/app/services/super_admin/resources/association_inspector.rb +112 -0
  59. data/app/services/super_admin/resources/collection_presenter.rb +63 -0
  60. data/app/services/super_admin/resources/context.rb +63 -0
  61. data/app/services/super_admin/resources/filter_params.rb +29 -0
  62. data/app/services/super_admin/resources/permitted_attributes.rb +104 -0
  63. data/app/services/super_admin/resources/value_normalizer.rb +121 -0
  64. data/app/services/super_admin/sensitive_attributes.rb +166 -0
  65. data/app/views/layouts/super_admin.html.erb +74 -0
  66. data/app/views/super_admin/audit_logs/index.html.erb +143 -0
  67. data/app/views/super_admin/dashboard/index.html.erb +79 -0
  68. data/app/views/super_admin/exports/index.html.erb +84 -0
  69. data/app/views/super_admin/exports/show.html.erb +57 -0
  70. data/app/views/super_admin/resources/_form.html.erb +42 -0
  71. data/app/views/super_admin/resources/destroy.turbo_stream.erb +17 -0
  72. data/app/views/super_admin/resources/edit.html.erb +37 -0
  73. data/app/views/super_admin/resources/index.html.erb +189 -0
  74. data/app/views/super_admin/resources/new.html.erb +31 -0
  75. data/app/views/super_admin/resources/show.html.erb +106 -0
  76. data/app/views/super_admin/shared/_breadcrumbs.html.erb +12 -0
  77. data/app/views/super_admin/shared/_custom_styles.html.erb +132 -0
  78. data/app/views/super_admin/shared/_flash.html.erb +55 -0
  79. data/app/views/super_admin/shared/_form_field.html.erb +35 -0
  80. data/app/views/super_admin/shared/_navigation.html.erb +92 -0
  81. data/app/views/super_admin/shared/_nested_fields.html.erb +59 -0
  82. data/app/views/super_admin/shared/_nested_record_fields.html.erb +45 -0
  83. data/config/importmap.rb +4 -0
  84. data/config/initializers/rack_attack.rb +134 -0
  85. data/config/initializers/super_admin.rb +117 -0
  86. data/config/locales/super_admin.en.yml +197 -0
  87. data/config/locales/super_admin.fr.yml +197 -0
  88. data/config/routes.rb +22 -0
  89. data/lib/generators/super_admin/dashboard_generator.rb +50 -0
  90. data/lib/generators/super_admin/install_generator.rb +58 -0
  91. data/lib/generators/super_admin/templates/20240101000001_create_super_admin_audit_logs.rb +24 -0
  92. data/lib/generators/super_admin/templates/20240101000002_create_super_admin_csv_exports.rb +33 -0
  93. data/lib/generators/super_admin/templates/super_admin.rb +58 -0
  94. data/lib/super_admin/dashboard_creator.rb +256 -0
  95. data/lib/super_admin/engine.rb +53 -0
  96. data/lib/super_admin/install_task.rb +96 -0
  97. data/lib/super_admin/version.rb +3 -0
  98. data/lib/super_admin.rb +7 -0
  99. data/lib/tasks/super_admin_tasks.rake +38 -0
  100. metadata +239 -0
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperAdmin
4
+ # Persists a trace of meaningful operations executed through SuperAdmin.
5
+ class AuditLog < ApplicationRecord
6
+ self.table_name = "super_admin_audit_logs"
7
+
8
+ belongs_to :user,
9
+ class_name: SuperAdmin.configuration.user_class_constant.name,
10
+ optional: true
11
+
12
+ validates :resource_type, :action, :performed_at, presence: true
13
+
14
+ scope :recent, -> { order(performed_at: :desc) }
15
+
16
+ before_validation :default_performed_at
17
+ before_validation :default_payloads
18
+
19
+ private
20
+
21
+ def default_performed_at
22
+ return if performed_at.present?
23
+
24
+ # When required attributes are missing, keep the field blank so validation errors surface.
25
+ return if resource_type.blank? || action.blank?
26
+
27
+ self.performed_at = Time.current
28
+ end
29
+
30
+ def default_payloads
31
+ self.changes_snapshot ||= {}
32
+ self.context ||= {}
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperAdmin
4
+ class CsvExport < ApplicationRecord
5
+ self.table_name = "super_admin_csv_exports"
6
+
7
+ RETENTION_PERIOD = 7.days unless const_defined?(:RETENTION_PERIOD)
8
+
9
+ belongs_to :user
10
+
11
+ has_one_attached :file
12
+
13
+ enum :status, {
14
+ pending: "pending",
15
+ processing: "processing",
16
+ ready: "ready",
17
+ failed: "failed"
18
+ }, suffix: true
19
+
20
+ validates :resource_name, :model_class_name, :status, :token, presence: true
21
+ validates :token, uniqueness: true
22
+
23
+ before_validation :generate_token, on: :create
24
+
25
+ scope :recent_first, -> { order(created_at: :desc) }
26
+ scope :active, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) }
27
+
28
+ def ready_for_download?
29
+ ready_status? && file.attached?
30
+ rescue NoMethodError, ActiveRecord::StatementInvalid
31
+ # Handle cases where ActiveStorage is unavailable or not initialized
32
+ false
33
+ end
34
+
35
+ def mark_processing!
36
+ update!(status: :processing, started_at: Time.current)
37
+ end
38
+
39
+ def mark_ready!(records_count:)
40
+ update!(
41
+ status: :ready,
42
+ records_count: records_count,
43
+ completed_at: Time.current,
44
+ expires_at: RETENTION_PERIOD.from_now
45
+ )
46
+ end
47
+
48
+ def mark_failed!(error_message)
49
+ update!(
50
+ status: :failed,
51
+ error_message: error_message.to_s.truncate(500),
52
+ completed_at: Time.current
53
+ )
54
+ end
55
+
56
+ private
57
+
58
+ # Generate a secure random token. The uniqueness validation and database unique index
59
+ # will prevent race conditions. With 24 bytes (192 bits), the collision probability
60
+ # is astronomically low (~1 in 10^57 for billions of records).
61
+ def generate_token
62
+ return if token.present?
63
+
64
+ self.token = SecureRandom.urlsafe_base64(24)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperAdmin
4
+ # Lightweight helper that records meaningful user actions inside SuperAdmin.
5
+ module Auditing
6
+ module_function
7
+
8
+ def log!(user:, resource: nil, resource_type: nil, resource_id: nil, action:, changes: nil, context: {})
9
+ resource_type ||= resource&.class&.name
10
+ resource_id ||= resource&.try(:id)&.to_s
11
+
12
+ return if resource_type.blank? || resource_type == "SuperAdmin::AuditLog"
13
+
14
+ SuperAdmin::AuditLog.create(
15
+ user: compatible_user(user),
16
+ user_email: safe_user_email(user),
17
+ resource_type: resource_type,
18
+ resource_id: resource_id,
19
+ action: action.to_s,
20
+ changes_snapshot: prepare_changes(resource, action, changes),
21
+ context: context.presence || {},
22
+ performed_at: Time.current
23
+ )
24
+ rescue StandardError => error
25
+ Rails.logger.error(
26
+ "[SuperAdmin::Auditing] Failed to log action #{action} on #{resource.class.name}: #{error.class} - #{error.message}"
27
+ )
28
+ nil
29
+ end
30
+
31
+ def compatible_user(user)
32
+ return nil unless user
33
+
34
+ user_class = SuperAdmin.configuration.user_class_constant
35
+ return user if user.is_a?(user_class)
36
+
37
+ nil
38
+ rescue SuperAdmin::ConfigurationError
39
+ nil
40
+ end
41
+
42
+ def safe_user_email(user)
43
+ user.respond_to?(:email) ? user.email : nil
44
+ end
45
+
46
+ def prepare_changes(resource, action, changes)
47
+ payload = if changes.present?
48
+ sanitize_changes(changes)
49
+ else
50
+ resource ? default_changes(resource, action) : {}
51
+ end
52
+ payload.presence || {}
53
+ end
54
+
55
+ def default_changes(resource, action)
56
+ case action.to_s
57
+ when "create"
58
+ { "after" => resource.attributes }
59
+ when "update"
60
+ sanitize_changes(resource.previous_changes)
61
+ when "destroy"
62
+ { "before" => resource.attributes }
63
+ else
64
+ {}
65
+ end
66
+ end
67
+
68
+ def sanitize_changes(changes)
69
+ return {} unless changes
70
+
71
+ changes.except("updated_at")
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperAdmin
4
+ # Authorization orchestrator. Selects the appropriate adapter and executes it.
5
+ class Authorization
6
+ class NotAuthorizedError < StandardError; end
7
+
8
+ class << self
9
+ def call(controller)
10
+ adapter = build_adapter(controller)
11
+ return true if adapter.authorized?
12
+
13
+ handle_unauthorized(controller, adapter)
14
+ false
15
+ end
16
+
17
+ def build_adapter(controller)
18
+ resolve_adapter(controller).new(controller)
19
+ end
20
+
21
+ private
22
+
23
+ def resolve_adapter(controller)
24
+ adapter_option = SuperAdmin.configuration.authorization_adapter
25
+
26
+ case adapter_option
27
+ when nil, :auto
28
+ auto_detect_adapter(controller)
29
+ when :default
30
+ config = SuperAdmin.configuration
31
+ config.authorize_with.present? ? AuthorizationAdapters::ProcAdapter : AuthorizationAdapters::DefaultAdapter
32
+ when Symbol, String
33
+ adapter_from_name(adapter_option)
34
+ when Class
35
+ adapter_option
36
+ else
37
+ AuthorizationAdapters::DefaultAdapter
38
+ end
39
+ rescue NameError => e
40
+ Rails.logger.error("[SuperAdmin] Authorization adapter resolution failed: #{e.message}")
41
+ AuthorizationAdapters::DefaultAdapter
42
+ end
43
+
44
+ def auto_detect_adapter(controller)
45
+ config = SuperAdmin.configuration
46
+
47
+ return AuthorizationAdapters::ProcAdapter if config.authorize_with.present?
48
+
49
+ if defined?(::Pundit) && controller.respond_to?(:authorize, true)
50
+ return AuthorizationAdapters::PunditAdapter
51
+ end
52
+
53
+ if defined?(::CanCan::Ability) && controller.respond_to?(:authorize!, true)
54
+ return AuthorizationAdapters::CancanAdapter
55
+ end
56
+
57
+ AuthorizationAdapters::DefaultAdapter
58
+ end
59
+
60
+ def adapter_from_name(name)
61
+ key = name.to_sym
62
+
63
+ case key
64
+ when :pundit
65
+ AuthorizationAdapters::PunditAdapter
66
+ when :cancan, :cancancan
67
+ AuthorizationAdapters::CancanAdapter
68
+ when :proc
69
+ AuthorizationAdapters::ProcAdapter
70
+ when :default
71
+ AuthorizationAdapters::DefaultAdapter
72
+ else
73
+ "SuperAdmin::AuthorizationAdapters::#{key.to_s.camelize}Adapter".constantize
74
+ end
75
+ end
76
+
77
+ def handle_unauthorized(controller, adapter)
78
+ handler = SuperAdmin.configuration.on_unauthorized
79
+ error = adapter.build_error
80
+
81
+ if handler
82
+ invoke_handler(controller, error, handler)
83
+ else
84
+ adapter.handle_unauthorized!(error)
85
+ end
86
+
87
+ error
88
+ end
89
+
90
+ def invoke_handler(controller, error, handler)
91
+ case handler
92
+ when Proc
93
+ args = case handler.arity
94
+ when 0
95
+ []
96
+ when 1
97
+ [ error ]
98
+ else
99
+ [ controller, error ]
100
+ end
101
+
102
+ controller.instance_exec(*args, &handler)
103
+ else
104
+ if controller.respond_to?(handler)
105
+ controller.public_send(handler, error)
106
+ else
107
+ raise ConfigurationError, "Unauthorized handler '#{handler}' is not defined"
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperAdmin
4
+ module AuthorizationAdapters
5
+ # Base class for authorization adapters. Provides helpers shared by all adapters.
6
+ class BaseAdapter
7
+ attr_reader :controller, :last_exception
8
+
9
+ def initialize(controller)
10
+ @controller = controller
11
+ @last_exception = nil
12
+ end
13
+
14
+ def authorized?(resource = nil)
15
+ raise NotImplementedError, "Adapters must implement #authorized?"
16
+ end
17
+
18
+ def authorize(resource = nil)
19
+ @last_exception = nil
20
+
21
+ result = invoke_authorized_check(resource)
22
+ !!result
23
+ rescue SuperAdmin::Authorization::NotAuthorizedError => error
24
+ remember_failure(error)
25
+ false
26
+ rescue StandardError => error
27
+ remember_failure(error)
28
+ raise
29
+ end
30
+
31
+ def authorized_scope(scope)
32
+ scope
33
+ end
34
+
35
+ # Default unauthorized handler simply raises the provided error. Adapters can override.
36
+ def handle_unauthorized!(error)
37
+ raise(error)
38
+ end
39
+
40
+ def build_error
41
+ SuperAdmin::Authorization::NotAuthorizedError.new(default_error_message)
42
+ end
43
+
44
+ protected
45
+
46
+ def remember_failure(exception = nil)
47
+ @last_exception = exception
48
+ false
49
+ end
50
+
51
+ def invoke_authorized_check(resource)
52
+ method = method(:authorized?)
53
+
54
+ if method.arity.zero?
55
+ authorized?
56
+ else
57
+ authorized?(resource)
58
+ end
59
+ end
60
+
61
+ def current_user
62
+ strategy = SuperAdmin.configuration.current_user_method || :current_user
63
+
64
+ case strategy
65
+ when Proc
66
+ controller.instance_exec(&strategy)
67
+ when Symbol, String
68
+ method_name = strategy.to_sym
69
+ return unless controller.respond_to?(method_name, true)
70
+
71
+ controller.__send__(method_name)
72
+ else
73
+ nil
74
+ end
75
+ rescue NoMethodError
76
+ nil
77
+ end
78
+
79
+ def default_error_message
80
+ I18n.t("super_admin.flash.access_denied", default: "You do not have permission to access this section.")
81
+ end
82
+
83
+ def redirect_with_alert!(message)
84
+ return unless controller.respond_to?(:redirect_to)
85
+
86
+ if controller.respond_to?(:flash)
87
+ controller.flash[:alert] ||= message
88
+ end
89
+
90
+ target = if controller.respond_to?(:main_app) && controller.main_app.respond_to?(:root_path)
91
+ controller.main_app.root_path
92
+ else
93
+ "/"
94
+ end
95
+
96
+ controller.redirect_to(target)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperAdmin
4
+ module AuthorizationAdapters
5
+ # Fallback authorization strategy relying on configured super admin predicate.
6
+ class DefaultAdapter < BaseAdapter
7
+ def authorized?(resource = nil)
8
+ checker = SuperAdmin.configuration.super_admin_check
9
+
10
+ if checker
11
+ user = current_user || resource
12
+ raise SuperAdmin::ConfigurationError, "Configure a #current_user method or provide a user resource" unless user
13
+
14
+ !!evaluate_custom_check(user, resource)
15
+ else
16
+ user = current_user || resource
17
+
18
+ if user && (user.respond_to?(:super_admin?) || user.respond_to?(:admin?))
19
+ !!evaluate_standard_predicate(user)
20
+ else
21
+ true
22
+ end
23
+ end
24
+ rescue SuperAdmin::ConfigurationError => error
25
+ raise error
26
+ rescue StandardError => error
27
+ raise SuperAdmin::ConfigurationError, "SuperAdmin authorization failed: #{error.message}"
28
+ end
29
+
30
+ def handle_unauthorized!(error)
31
+ redirect_with_alert!(error.message)
32
+ end
33
+
34
+ private
35
+
36
+ def evaluate_custom_check(user, resource)
37
+ checker = SuperAdmin.configuration.super_admin_check
38
+
39
+ case checker
40
+ when Proc
41
+ case checker.arity
42
+ when 0
43
+ controller.instance_exec(&checker)
44
+ when 1
45
+ controller.instance_exec(user, &checker)
46
+ when 2
47
+ controller.instance_exec(controller, user, &checker)
48
+ else
49
+ controller.instance_exec(controller, user, resource, &checker)
50
+ end
51
+ when Symbol, String
52
+ predicate = checker.to_sym
53
+
54
+ if user.respond_to?(predicate)
55
+ user.public_send(predicate)
56
+ elsif controller.respond_to?(predicate, true)
57
+ controller.__send__(predicate, user)
58
+ else
59
+ raise SuperAdmin::ConfigurationError, "SuperAdmin predicate '#{predicate}' is not defined"
60
+ end
61
+ else
62
+ raise SuperAdmin::ConfigurationError, "Unsupported super_admin_check: #{checker.inspect}"
63
+ end
64
+ end
65
+
66
+ def evaluate_standard_predicate(user)
67
+ if user.respond_to?(:super_admin?)
68
+ user.super_admin?
69
+ elsif user.respond_to?(:admin?)
70
+ user.admin?
71
+ else
72
+ raise SuperAdmin::ConfigurationError, "Define SuperAdmin.super_admin_check or add #super_admin?/#admin? predicate to the user"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperAdmin
4
+ module AuthorizationAdapters
5
+ # Adapter executing a custom callable configured via SuperAdmin.configure { |c| c.authorize_with = ... }.
6
+ class ProcAdapter < BaseAdapter
7
+ def authorized?(resource = nil)
8
+ callable = SuperAdmin.configuration.authorize_with
9
+ raise SuperAdmin::ConfigurationError, "authorize_with must be a callable or method name" if callable.blank?
10
+
11
+ result = case callable
12
+ when Proc
13
+ invoke_proc(callable, resource)
14
+ when Symbol, String
15
+ invoke_method(callable.to_sym)
16
+ else
17
+ raise SuperAdmin::ConfigurationError, "Unsupported authorize_with value: #{callable.inspect}"
18
+ end
19
+
20
+ !!result
21
+ rescue SuperAdmin::ConfigurationError
22
+ raise
23
+ rescue StandardError => exception
24
+ remember_failure(exception)
25
+ end
26
+
27
+ def build_error
28
+ message = if last_exception&.respond_to?(:message) && last_exception.message.present?
29
+ last_exception.message
30
+ else
31
+ default_error_message
32
+ end
33
+
34
+ SuperAdmin::Authorization::NotAuthorizedError.new(message)
35
+ end
36
+
37
+ def handle_unauthorized!(error)
38
+ redirect_with_alert!(error.message)
39
+ end
40
+
41
+ private
42
+
43
+ def invoke_proc(callable, resource)
44
+ positional_count = callable.parameters.count { |type, _| %i[req opt].include?(type) }
45
+ rest_parameter = callable.parameters.any? { |type, _| type == :rest }
46
+
47
+ base_arguments = [ controller, resource, current_user ]
48
+ args = base_arguments.first(positional_count)
49
+ args += base_arguments.drop(positional_count) if rest_parameter
50
+
51
+ controller.instance_exec(*args, &callable)
52
+ end
53
+
54
+ def invoke_method(method_name)
55
+ if controller.respond_to?(method_name, true)
56
+ controller.__send__(method_name)
57
+ elsif current_user&.respond_to?(method_name)
58
+ current_user.public_send(method_name)
59
+ else
60
+ raise SuperAdmin::ConfigurationError, "authorize_with method '#{method_name}' is not defined"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperAdmin
4
+ module AuthorizationAdapters
5
+ # Adapter delegating authorization to Pundit.
6
+ class PunditAdapter < BaseAdapter
7
+ def authorized?(resource = nil)
8
+ ensure_pundit_available!
9
+ subject = resource || :super_admin
10
+ controller.__send__(:authorize, subject, :access?)
11
+ true
12
+ rescue LoadError => exception
13
+ raise SuperAdmin::ConfigurationError, missing_policy_message(exception)
14
+ rescue StandardError => exception
15
+ if pundit_not_authorized?(exception)
16
+ return remember_failure(exception)
17
+ end
18
+
19
+ if policy_missing?(exception)
20
+ raise SuperAdmin::ConfigurationError, missing_policy_message(exception)
21
+ end
22
+
23
+ raise
24
+ end
25
+
26
+ def authorized_scope(scope)
27
+ ensure_pundit_available!
28
+ return scope unless controller.respond_to?(:policy_scope)
29
+
30
+ controller.policy_scope(scope)
31
+ rescue LoadError => exception
32
+ raise SuperAdmin::ConfigurationError, missing_policy_message(exception)
33
+ rescue StandardError => exception
34
+ if policy_missing?(exception)
35
+ raise SuperAdmin::ConfigurationError, missing_policy_message(exception)
36
+ end
37
+
38
+ raise
39
+ end
40
+
41
+ def build_error
42
+ error = SuperAdmin::Authorization::NotAuthorizedError.new(default_error_message)
43
+ attach_cause(error)
44
+ error
45
+ end
46
+
47
+ def handle_unauthorized!(error)
48
+ redirect_with_alert!(error.message)
49
+ end
50
+
51
+ private
52
+
53
+ def ensure_pundit_available!
54
+ return if defined?(::Pundit)
55
+ return if controller.respond_to?(:authorize)
56
+
57
+ raise SuperAdmin::ConfigurationError, "Pundit adapter selected but Pundit is not loaded"
58
+ end
59
+
60
+ def pundit_not_authorized?(exception)
61
+ defined?(::Pundit::NotAuthorizedError) && exception.is_a?(::Pundit::NotAuthorizedError)
62
+ end
63
+
64
+ def attach_cause(error)
65
+ return unless last_exception
66
+
67
+ cause = last_exception
68
+ error.define_singleton_method(:cause) { cause }
69
+ error.set_backtrace(cause.backtrace) if cause.backtrace
70
+ end
71
+
72
+ def missing_policy_message(exception)
73
+ "SuperAdminPolicy with #access? must exist when using the Pundit adapter. #{exception.message}"
74
+ end
75
+
76
+ def policy_missing?(exception)
77
+ defined?(::Pundit::NotDefinedError) && exception.is_a?(::Pundit::NotDefinedError)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperAdmin
4
+ # Encapsulates CSV export creation logic and enqueues it in Solid Queue.
5
+ class CsvExportCreator
6
+ DEFAULT_RETENTION = 7.days
7
+
8
+ def self.call(**kwargs)
9
+ new(**kwargs).call
10
+ end
11
+
12
+ def initialize(user:, model_class:, resource_name: nil, resource: nil, scope: nil, search: nil, sort: nil, direction: nil, filters: {}, attributes: [])
13
+ @user = user
14
+ @model_class = model_class
15
+ @resource_name = (resource || resource_name || model_class.model_name.plural).to_s
16
+ @scope = scope || model_class.all
17
+ @search = search
18
+ @sort = sort
19
+ @direction = direction
20
+ @filters = filters || {}
21
+ @attributes = Array(attributes).map(&:to_s)
22
+ end
23
+
24
+ def call
25
+ export = @user.csv_exports.create!(
26
+ model_class_name: @model_class.name,
27
+ resource_name: @resource_name,
28
+ search: @search,
29
+ sort: @sort,
30
+ direction: @direction,
31
+ filters: @filters,
32
+ selected_attributes: @attributes,
33
+ expires_at: DEFAULT_RETENTION.from_now
34
+ )
35
+
36
+ SuperAdmin::GenerateSuperAdminCsvExportJob.perform_later(
37
+ export_id: export.id,
38
+ model_class_name: @model_class.name,
39
+ attributes: @attributes
40
+ )
41
+
42
+ export
43
+ end
44
+ end
45
+ end