dscf-core 0.2.8 → 0.2.9

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/dscf/core/auditable_controller.rb +17 -17
  3. data/app/controllers/concerns/dscf/core/authenticatable.rb +3 -8
  4. data/app/controllers/concerns/dscf/core/authorizable.rb +66 -0
  5. data/app/controllers/concerns/dscf/core/common.rb +49 -19
  6. data/app/controllers/concerns/dscf/core/file_uploadable.rb +261 -0
  7. data/app/controllers/concerns/dscf/core/reviewable_controller.rb +31 -1
  8. data/app/controllers/dscf/core/addresses_controller.rb +0 -5
  9. data/app/controllers/dscf/core/application_controller.rb +3 -5
  10. data/app/controllers/dscf/core/auth_controller.rb +16 -11
  11. data/app/controllers/dscf/core/businesses_controller.rb +0 -5
  12. data/app/controllers/dscf/core/files_controller.rb +38 -0
  13. data/app/controllers/dscf/core/permissions_controller.rb +20 -0
  14. data/app/controllers/dscf/core/role_permissions_controller.rb +54 -0
  15. data/app/controllers/dscf/core/roles_controller.rb +30 -0
  16. data/app/controllers/dscf/core/user_roles_controller.rb +56 -0
  17. data/app/errors/dscf/core/file_upload_error.rb +9 -0
  18. data/app/jobs/dscf/core/audit_logger_job.rb +1 -3
  19. data/app/models/concerns/dscf/core/attachable.rb +403 -0
  20. data/app/models/dscf/core/file_attachment.rb +205 -0
  21. data/app/models/dscf/core/permission.rb +27 -0
  22. data/app/models/dscf/core/role.rb +8 -3
  23. data/app/models/dscf/core/role_permission.rb +18 -0
  24. data/app/models/dscf/core/user.rb +56 -0
  25. data/app/models/dscf/core/user_role.rb +23 -3
  26. data/app/policies/dscf/core/application_policy.rb +62 -0
  27. data/app/policies/dscf/core/business_policy.rb +29 -0
  28. data/app/policies/dscf/core/business_type_policy.rb +6 -0
  29. data/app/policies/dscf/core/permission_policy.rb +6 -0
  30. data/app/policies/dscf/core/role_policy.rb +25 -0
  31. data/app/serializers/dscf/core/attachment_serializer.rb +30 -0
  32. data/app/serializers/dscf/core/permission_serializer.rb +7 -0
  33. data/app/serializers/dscf/core/role_light_serializer.rb +7 -0
  34. data/app/serializers/dscf/core/role_permission_serializer.rb +9 -0
  35. data/app/serializers/dscf/core/role_serializer.rb +2 -2
  36. data/app/serializers/dscf/core/user_auth_serializer.rb +6 -2
  37. data/app/serializers/dscf/core/user_role_serializer.rb +2 -3
  38. data/app/services/dscf/core/file_storage/client.rb +210 -0
  39. data/app/services/dscf/core/file_storage/uploader.rb +127 -0
  40. data/app/services/dscf/core/file_storage.rb +44 -0
  41. data/app/services/dscf/core/token_service.rb +2 -1
  42. data/config/locales/en.yml +35 -2
  43. data/config/routes.rb +15 -0
  44. data/db/migrate/20250821185708_create_dscf_core_roles.rb +3 -1
  45. data/db/migrate/20250822054547_create_dscf_core_user_roles.rb +3 -1
  46. data/db/migrate/20260128000000_create_dscf_core_file_attachments.rb +48 -0
  47. data/db/migrate/20260304000001_create_dscf_core_permissions.rb +19 -0
  48. data/db/migrate/20260304000002_create_dscf_core_role_permissions.rb +11 -0
  49. data/lib/dscf/core/permission_registry.rb +58 -0
  50. data/lib/dscf/core/version.rb +1 -1
  51. data/lib/dscf/core.rb +12 -1
  52. data/spec/factories/dscf/core/permissions.rb +14 -0
  53. data/spec/factories/dscf/core/reviews.rb +0 -1
  54. data/spec/factories/dscf/core/role_permissions.rb +6 -0
  55. data/spec/factories/dscf/core/roles.rb +42 -2
  56. data/spec/factories/dscf/core/user_roles.rb +4 -2
  57. metadata +33 -3
@@ -0,0 +1,403 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dscf
4
+ module Core
5
+ # Include this concern in any model to add file attachment capabilities
6
+ # Works exactly like ActiveStorage's has_one_attached and has_many_attached
7
+ # NO MIGRATION NEEDED per model - just include and declare!
8
+ #
9
+ # @example Single file attachment
10
+ # class UserProfile < ApplicationRecord
11
+ # include Dscf::Core::Attachable
12
+ #
13
+ # has_one_file :avatar
14
+ # has_one_file :cover_photo, max_size: 10.megabytes
15
+ # end
16
+ #
17
+ # profile.avatar.attach(params[:avatar])
18
+ # profile.avatar.attached? # => true
19
+ # profile.avatar.url # => "/dscf/core/files/..."
20
+ # profile.avatar.download # => binary data
21
+ # profile.avatar.purge # deletes file
22
+ #
23
+ # @example Multiple files attachment
24
+ # class Product < ApplicationRecord
25
+ # include Dscf::Core::Attachable
26
+ #
27
+ # has_many_files :images
28
+ # has_many_files :documents, allowed_types: %w[application/pdf]
29
+ # end
30
+ #
31
+ # product.images.attach(params[:images])
32
+ # product.images.attached? # => true
33
+ # product.images.count # => 3
34
+ # product.images.first.url # => "/dscf/core/files/..."
35
+ # product.images.purge_all # deletes all
36
+ #
37
+ module Attachable
38
+ extend ActiveSupport::Concern
39
+
40
+ included do
41
+ # Polymorphic association to the central file_attachments table
42
+ has_many :file_attachments,
43
+ class_name: "Dscf::Core::FileAttachment",
44
+ as: :attachable,
45
+ dependent: :destroy
46
+
47
+ # Store attachment configurations
48
+ class_attribute :_file_attachment_configs, default: {}
49
+ end
50
+
51
+ class_methods do
52
+ # Define a single file attachment
53
+ # @param name [Symbol] attachment name
54
+ # @param options [Hash] configuration options
55
+ # @option options [Integer] :max_size maximum file size (default: 5MB)
56
+ # @option options [Array<String>] :allowed_types allowed MIME types
57
+ def has_one_file(name, **options)
58
+ _file_attachment_configs[name] = options.merge(multiple: false)
59
+ define_single_attachment_methods(name, options)
60
+ end
61
+
62
+ # Define a multiple file attachment
63
+ # @param name [Symbol] attachment name
64
+ # @param options [Hash] configuration options
65
+ def has_many_files(name, **options)
66
+ _file_attachment_configs[name] = options.merge(multiple: true)
67
+ define_multiple_attachment_methods(name, options)
68
+ end
69
+
70
+ private
71
+
72
+ def define_single_attachment_methods(name, options)
73
+ # Getter: returns a SingleAttachmentProxy (memoized to preserve state like errors)
74
+ ivar_name = "@_attachment_proxy_#{name}"
75
+ define_method(name) do
76
+ instance_variable_get(ivar_name) ||
77
+ instance_variable_set(ivar_name, SingleAttachmentProxy.new(self, name, options))
78
+ end
79
+
80
+ # Direct setter for convenience: model.avatar = file
81
+ define_method("#{name}=") do |file|
82
+ send(name).attach(file)
83
+ end
84
+ end
85
+
86
+ def define_multiple_attachment_methods(name, options)
87
+ # Getter: returns a MultipleAttachmentProxy (memoized to preserve state)
88
+ ivar_name = "@_attachment_proxy_#{name}"
89
+ define_method(name) do
90
+ instance_variable_get(ivar_name) ||
91
+ instance_variable_set(ivar_name, MultipleAttachmentProxy.new(self, name, options))
92
+ end
93
+ define_method("#{name}=") do |files|
94
+ send(name).attach(files)
95
+ end
96
+ end
97
+ end
98
+
99
+ # ==========================================================================
100
+ # SingleAttachmentProxy - handles has_one_file attachments
101
+ # ==========================================================================
102
+ class SingleAttachmentProxy
103
+ # Delegate common methods to the underlying FileAttachment record
104
+ delegate :file_key, :filename, :content_type, :size, :human_size,
105
+ :extension, :image?, :pdf?, :video?, :url, :download,
106
+ :metadata, :created_at, :id,
107
+ to: :attachment, allow_nil: true
108
+
109
+ def initialize(record, name, options)
110
+ @record = record
111
+ @name = name.to_s
112
+ @options = options
113
+ @errors = []
114
+ @uploader_context = nil
115
+ end
116
+
117
+ # Set the user who is uploading the file
118
+ # @param user [ActiveRecord::Base] the user performing the upload
119
+ # @return [self]
120
+ def uploaded_by(user)
121
+ @uploader_context = user
122
+ self
123
+ end
124
+
125
+ # Attach a file to this record
126
+ # @param file [ActionDispatch::Http::UploadedFile, File, Hash, nil]
127
+ # @return [Boolean] success status
128
+ def attach(file)
129
+ @errors = []
130
+ return detach if file.nil?
131
+
132
+ # Validate and upload the file (pass record class for engine detection)
133
+ uploader = FileStorage::Uploader.new(uploader_options.merge(record_class: @record.class))
134
+ result = uploader.upload(file)
135
+
136
+ unless result
137
+ @errors = uploader.errors
138
+ return false
139
+ end
140
+
141
+ # Remove existing attachment (if any)
142
+ existing_attachment&.purge
143
+
144
+ # Create new attachment record
145
+ @record.file_attachments.create!(
146
+ name: @name,
147
+ file_key: result[:file_key],
148
+ filename: result[:filename],
149
+ content_type: result[:content_type],
150
+ size: result[:size],
151
+ metadata: result[:metadata] || {},
152
+ uploaded_by: @uploader_context
153
+ )
154
+
155
+ @attachment = nil # Reset cache
156
+ @uploader_context = nil # Reset uploader context
157
+ true
158
+ rescue StandardError => e
159
+ Rails.logger.error("Failed to attach file: #{e.message}")
160
+ @errors << "Failed to attach file: #{e.message}"
161
+ false
162
+ end
163
+
164
+ # Remove the attached file
165
+ # @return [Boolean]
166
+ def detach
167
+ existing_attachment&.purge
168
+ @attachment = nil
169
+ true
170
+ end
171
+
172
+ alias purge detach
173
+
174
+ # Check if a file is attached
175
+ # @return [Boolean]
176
+ def attached?
177
+ attachment.present? && attachment.attached?
178
+ end
179
+
180
+ # Get the underlying FileAttachment record
181
+ # @return [FileAttachment, nil]
182
+ def attachment
183
+ @attachment ||= @record.file_attachments.with_name(@name).first
184
+ end
185
+
186
+ # Check if file exists in storage
187
+ # @return [Boolean]
188
+ def exists?
189
+ attachment&.exists_in_storage? || false
190
+ end
191
+
192
+ # Get upload errors
193
+ # @return [Array<String>]
194
+ attr_reader :errors
195
+
196
+ # Serialize for JSON
197
+ # @return [Hash, nil]
198
+ def as_json(options = {})
199
+ return nil unless attached?
200
+
201
+ attachment.as_json(options)
202
+ end
203
+
204
+ private
205
+
206
+ def existing_attachment
207
+ @record.file_attachments.find_by(name: @name)
208
+ end
209
+
210
+ def uploader_options
211
+ opts = {}
212
+ opts[:max_size] = @options[:max_size] if @options[:max_size]
213
+ opts[:allowed_types] = @options[:allowed_types] if @options[:allowed_types]
214
+ opts
215
+ end
216
+ end
217
+
218
+ # ==========================================================================
219
+ # MultipleAttachmentProxy - handles has_many_files attachments
220
+ # ==========================================================================
221
+ class MultipleAttachmentProxy
222
+ include Enumerable
223
+
224
+ delegate :each, to: :attachments
225
+
226
+ def initialize(record, name, options)
227
+ @record = record
228
+ @name = name.to_s
229
+ @options = options
230
+ @errors = []
231
+ @uploader_context = nil
232
+ end
233
+
234
+ # Set the user who is uploading the files
235
+ # @param user [ActiveRecord::Base] the user performing the upload
236
+ # @return [self]
237
+ def uploaded_by(user)
238
+ @uploader_context = user
239
+ self
240
+ end
241
+
242
+ # Attach one or more files
243
+ # @param files [ActionDispatch::Http::UploadedFile, Array<ActionDispatch::Http::UploadedFile>]
244
+ # @param replace [Boolean] whether to replace existing files (default: false)
245
+ # @return [Boolean] success status
246
+ def attach(files, replace: false)
247
+ @errors = []
248
+ files = Array(files).compact
249
+ return true if files.empty?
250
+
251
+ # Purge existing if replacing
252
+ purge_all if replace
253
+
254
+ uploader = FileStorage::Uploader.new(uploader_options)
255
+ current_position = attachments.maximum(:position).to_i
256
+
257
+ files.each do |file|
258
+ result = uploader.upload(file)
259
+
260
+ if result
261
+ current_position += 1
262
+ @record.file_attachments.create!(
263
+ name: @name,
264
+ file_key: result[:file_key],
265
+ filename: result[:filename],
266
+ content_type: result[:content_type],
267
+ size: result[:size],
268
+ metadata: result[:metadata] || {},
269
+ position: current_position,
270
+ uploaded_by: @uploader_context
271
+ )
272
+ else
273
+ @errors.concat(uploader.errors)
274
+ end
275
+ end
276
+
277
+ @attachments = nil # Reset cache
278
+ @uploader_context = nil # Reset uploader context
279
+ @errors.empty?
280
+ rescue StandardError => e
281
+ Rails.logger.error("Failed to attach files: #{e.message}")
282
+ @errors << "Failed to attach files: #{e.message}"
283
+ false
284
+ end
285
+
286
+ # Remove a specific file by its file_key
287
+ # @param file_key [String]
288
+ # @return [Boolean]
289
+ def detach(file_key)
290
+ att = attachments.find_by(file_key: file_key)
291
+ return false unless att
292
+
293
+ att.purge
294
+ @attachments = nil
295
+ true
296
+ end
297
+
298
+ # Remove all attached files
299
+ # @return [Boolean]
300
+ def purge_all
301
+ attachments.each(&:purge)
302
+ @attachments = nil
303
+ true
304
+ end
305
+
306
+ # Check if any files are attached
307
+ # @return [Boolean]
308
+ def attached?
309
+ attachments.exists?
310
+ end
311
+
312
+ # Get all FileAttachment records for this attachment name
313
+ # @return [ActiveRecord::Relation]
314
+ def attachments
315
+ @attachments ||= @record.file_attachments.with_name(@name).ordered
316
+ end
317
+
318
+ # Get count of attached files
319
+ # @return [Integer]
320
+ def count
321
+ attachments.count
322
+ end
323
+
324
+ alias size count
325
+ alias length count
326
+
327
+ # Check if empty
328
+ # @return [Boolean]
329
+ def empty?
330
+ !attached?
331
+ end
332
+
333
+ # Get first attachment
334
+ # @return [FileAttachment, nil]
335
+ def first
336
+ attachments.first
337
+ end
338
+
339
+ # Get last attachment
340
+ # @return [FileAttachment, nil]
341
+ def last
342
+ attachments.last
343
+ end
344
+
345
+ # Find attachment by file_key
346
+ # @param file_key [String]
347
+ # @return [FileAttachment, nil]
348
+ def find_by_key(file_key)
349
+ attachments.find_by(file_key: file_key)
350
+ end
351
+
352
+ # Filter to only images
353
+ # @return [ActiveRecord::Relation]
354
+ def images
355
+ attachments.images
356
+ end
357
+
358
+ # Filter to only PDFs
359
+ # @return [ActiveRecord::Relation]
360
+ def pdfs
361
+ attachments.pdfs
362
+ end
363
+
364
+ # Filter to only videos
365
+ # @return [ActiveRecord::Relation]
366
+ def videos
367
+ attachments.videos
368
+ end
369
+
370
+ # Get total size of all attachments
371
+ # @return [Integer]
372
+ def total_size
373
+ attachments.sum(:size)
374
+ end
375
+
376
+ # Get human-readable total size
377
+ # @return [String]
378
+ def human_total_size
379
+ ActiveSupport::NumberHelper.number_to_human_size(total_size)
380
+ end
381
+
382
+ # Get upload errors
383
+ # @return [Array<String>]
384
+ attr_reader :errors
385
+
386
+ # Serialize for JSON
387
+ # @return [Array<Hash>]
388
+ def as_json(options = {})
389
+ attachments.map { |att| att.as_json(options) }
390
+ end
391
+
392
+ private
393
+
394
+ def uploader_options
395
+ opts = {}
396
+ opts[:max_size] = @options[:max_size] if @options[:max_size]
397
+ opts[:allowed_types] = @options[:allowed_types] if @options[:allowed_types]
398
+ opts
399
+ end
400
+ end
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dscf
4
+ module Core
5
+ # FileAttachment model - stores file metadata with polymorphic association
6
+ # This is the central table for ALL file attachments across ALL engines
7
+ #
8
+ # @example Direct usage (rarely needed)
9
+ # attachment = Dscf::Core::FileAttachment.find(1)
10
+ # attachment.download # => binary data
11
+ # attachment.url # => "/dscf/core/files/..."
12
+ # attachment.purge # => deletes from MinIO and database
13
+ #
14
+ # @example Querying across all attachments
15
+ # Dscf::Core::FileAttachment.images # => all image attachments
16
+ # Dscf::Core::FileAttachment.pdfs # => all PDF attachments
17
+ # Dscf::Core::FileAttachment.sum(:size) # => total storage used
18
+ #
19
+ class FileAttachment < ApplicationRecord
20
+ # Associations
21
+ belongs_to :attachable, polymorphic: true
22
+ belongs_to :uploaded_by, polymorphic: true, optional: true
23
+
24
+ # Validations
25
+ validates :name, presence: true
26
+ validates :file_key, presence: true, uniqueness: true
27
+ validates :filename, presence: true
28
+ validates :content_type, presence: true
29
+ validates :size, presence: true, numericality: {greater_than: 0}
30
+ validates :attachable_type, presence: true
31
+ validates :attachable_id, presence: true
32
+
33
+ # Callbacks
34
+ before_destroy :purge_from_storage
35
+
36
+ # Scopes
37
+ scope :with_name, ->(name) { where(name: name.to_s) }
38
+ scope :images, -> { where("content_type LIKE ?", "image/%") }
39
+ scope :pdfs, -> { where(content_type: "application/pdf") }
40
+ scope :videos, -> { where("content_type LIKE ?", "video/%") }
41
+ scope :ordered, -> { order(position: :asc, created_at: :asc) }
42
+ scope :recent, -> { order(created_at: :desc) }
43
+ scope :by_type, ->(type) { where(content_type: type) }
44
+ scope :larger_than, ->(bytes) { where("size > ?", bytes) }
45
+ scope :smaller_than, ->(bytes) { where("size < ?", bytes) }
46
+ scope :uploaded_by, ->(record) { where(uploaded_by: record) }
47
+
48
+ # Class methods for analytics
49
+ class << self
50
+ def total_storage
51
+ sum(:size) || 0
52
+ end
53
+
54
+ # Ransack configuration for filtering
55
+ def ransackable_attributes(_auth_object = nil)
56
+ %w[id name filename content_type size attachable_type attachable_id
57
+ uploaded_by_type uploaded_by_id position created_at updated_at]
58
+ end
59
+
60
+ def ransackable_associations(_auth_object = nil)
61
+ %w[attachable uploaded_by]
62
+ end
63
+
64
+ def storage_by_model
65
+ group(:attachable_type).sum(:size)
66
+ end
67
+
68
+ def storage_by_content_type
69
+ group(:content_type).sum(:size)
70
+ end
71
+
72
+ def count_by_model
73
+ group(:attachable_type).count
74
+ end
75
+ end
76
+
77
+ # Download the actual file content from MinIO
78
+ # @return [String] binary file data
79
+ def download
80
+ result = storage_client.download(file_key)
81
+ result[:data]
82
+ rescue FileStorage::Client::DownloadError => e
83
+ Rails.logger.error("Failed to download file #{file_key}: #{e.message}")
84
+ nil
85
+ end
86
+
87
+ # Get the download URL for this attachment
88
+ # Works across all engines that mount dscf-core
89
+ # @param _expires_in [ActiveSupport::Duration] URL expiration time (reserved for future signed URLs)
90
+ # @return [String] the download URL
91
+ def url(_expires_in: 1.hour)
92
+ # Try to get URL through main app's mounted engine routes (most reliable)
93
+ if defined?(Rails.application) && Rails.application.routes.named_routes.key?(:dscf_core)
94
+ Rails.application.routes.url_helpers.dscf_core.file_download_path(file_key: file_key)
95
+ else
96
+ # Fallback: Use engine routes directly (works when engine is the main app or in tests)
97
+ Dscf::Core::Engine.routes.url_helpers.file_download_path(file_key: file_key)
98
+ end
99
+ rescue StandardError
100
+ # Last resort fallback with configurable base path
101
+ base_path = Dscf::Core.file_download_base_path
102
+ "#{base_path}/#{file_key}"
103
+ end
104
+
105
+ # Check if the file exists in MinIO storage
106
+ # @return [Boolean]
107
+ def exists_in_storage?
108
+ storage_client.exists?(file_key)
109
+ rescue StandardError
110
+ false
111
+ end
112
+
113
+ # Get the file extension
114
+ # @return [String, nil]
115
+ def extension
116
+ return nil unless filename
117
+
118
+ ext = File.extname(filename).delete_prefix(".")
119
+ ext.presence
120
+ end
121
+
122
+ # Get human-readable file size
123
+ # @return [String, nil]
124
+ def human_size
125
+ return nil unless size
126
+
127
+ ActiveSupport::NumberHelper.number_to_human_size(size)
128
+ end
129
+
130
+ # Check if this is an image file
131
+ # @return [Boolean]
132
+ def image?
133
+ content_type&.start_with?("image/")
134
+ end
135
+
136
+ # Check if this is a PDF file
137
+ # @return [Boolean]
138
+ def pdf?
139
+ content_type == "application/pdf"
140
+ end
141
+
142
+ # Check if this is a video file
143
+ # @return [Boolean]
144
+ def video?
145
+ content_type&.start_with?("video/")
146
+ end
147
+
148
+ # Check if attachment is properly persisted
149
+ # @return [Boolean]
150
+ def attached?
151
+ persisted? && file_key.present?
152
+ end
153
+
154
+ # Delete file from MinIO and destroy the record
155
+ # @return [Boolean]
156
+ def purge
157
+ purge_from_storage
158
+ destroy
159
+ end
160
+
161
+ # Delete file from MinIO and destroy the record (bang version)
162
+ # @return [Boolean]
163
+ def purge!
164
+ purge_from_storage
165
+ destroy!
166
+ end
167
+
168
+ # Serialize for JSON API responses
169
+ # @return [Hash]
170
+ def as_json(_options = {})
171
+ {
172
+ id: id,
173
+ file_key: file_key,
174
+ filename: filename,
175
+ content_type: content_type,
176
+ size: size,
177
+ human_size: human_size,
178
+ extension: extension,
179
+ is_image: image?,
180
+ is_pdf: pdf?,
181
+ is_video: video?,
182
+ url: url,
183
+ position: position,
184
+ created_at: created_at&.iso8601,
185
+ metadata: metadata
186
+ }
187
+ end
188
+
189
+ private
190
+
191
+ def storage_client
192
+ @storage_client ||= FileStorage::Client.new
193
+ end
194
+
195
+ def purge_from_storage
196
+ return unless file_key.present?
197
+
198
+ storage_client.delete(file_key)
199
+ rescue StandardError => e
200
+ Rails.logger.error("Failed to purge file #{file_key} from storage: #{e.message}")
201
+ # Don't raise - allow the database record to be deleted even if MinIO fails
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,27 @@
1
+ module Dscf
2
+ module Core
3
+ class Permission < ApplicationRecord
4
+ validates :code, presence: true, uniqueness: {case_sensitive: false}
5
+ validates :resource, presence: true
6
+ validates :action, presence: true
7
+ validates :engine, presence: true
8
+ validates :resource, uniqueness: {scope: %i[action engine], message: "action and engine combination already exists"}
9
+
10
+ has_many :role_permissions, class_name: "Dscf::Core::RolePermission", dependent: :destroy
11
+ has_many :roles, through: :role_permissions, class_name: "Dscf::Core::Role"
12
+
13
+ scope :active, -> { where(active: true) }
14
+ scope :inactive, -> { where(active: false) }
15
+ scope :by_resource, ->(resource) { where(resource: resource.to_s) }
16
+ scope :by_engine, ->(engine) { where(engine: engine.to_s) }
17
+
18
+ def self.ransackable_attributes(_auth_object = nil)
19
+ %w[id code resource action engine description active created_at updated_at]
20
+ end
21
+
22
+ def self.ransackable_associations(_auth_object = nil)
23
+ %w[role_permissions roles]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -4,15 +4,20 @@ module Dscf
4
4
  validates :code, presence: true, uniqueness: true
5
5
  validates :name, presence: true
6
6
 
7
- has_many :user_roles, class_name: "Dscf::Core::UserRole"
7
+ has_many :user_roles, class_name: "Dscf::Core::UserRole", dependent: :destroy
8
8
  has_many :users, through: :user_roles, class_name: "Dscf::Core::User"
9
+ has_many :role_permissions, class_name: "Dscf::Core::RolePermission", dependent: :destroy
10
+ has_many :permissions, through: :role_permissions, class_name: "Dscf::Core::Permission"
11
+
12
+ scope :active, -> { where(active: true) }
13
+ scope :inactive, -> { where(active: false) }
9
14
 
10
15
  def self.ransackable_attributes(_auth_object = nil)
11
- %w[id code name created_at updated_at]
16
+ %w[id code name description active created_at updated_at]
12
17
  end
13
18
 
14
19
  def self.ransackable_associations(_auth_object = nil)
15
- %w[user_roles users reviews]
20
+ %w[user_roles users role_permissions permissions]
16
21
  end
17
22
  end
18
23
  end
@@ -0,0 +1,18 @@
1
+ module Dscf
2
+ module Core
3
+ class RolePermission < ApplicationRecord
4
+ belongs_to :role, class_name: "Dscf::Core::Role"
5
+ belongs_to :permission, class_name: "Dscf::Core::Permission"
6
+
7
+ validates :permission_id, uniqueness: {scope: :role_id, message: "already assigned to this role"}
8
+
9
+ def self.ransackable_attributes(_auth_object = nil)
10
+ %w[id role_id permission_id created_at updated_at]
11
+ end
12
+
13
+ def self.ransackable_associations(_auth_object = nil)
14
+ %w[role permission]
15
+ end
16
+ end
17
+ end
18
+ end