propel_api 0.3.1.6 → 0.3.3

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.
@@ -37,6 +37,9 @@ module PropelApi
37
37
 
38
38
  # Optionally enhance CSRF error messages (non-breaking)
39
39
  add_csrf_error_handling
40
+
41
+ # Add dynamic filtering capabilities to ApplicationRecord
42
+ add_model_filters_concern
40
43
  end
41
44
 
42
45
  case @adapter
@@ -113,6 +116,28 @@ module PropelApi
113
116
 
114
117
  private
115
118
 
119
+ def copy_propel_facets_concerns
120
+ if behavior == :revoke
121
+ # Remove concerns
122
+ remove_file "app/controllers/concerns/propel_controller_filters_concern.rb"
123
+ remove_file "app/models/concerns/propel_model_filters_concern.rb"
124
+ remove_file "lib/propel_filter_operators.rb"
125
+ remove_file "lib/propel_dynamic_scope_generator.rb"
126
+ say "šŸ—‘ļø Removed PropelApi filtering concerns", :red
127
+ else
128
+ # Copy concerns to host app using XX-prefixed source files
129
+ copy_file "concerns/propel_controller_filters_concern.rb", "app/controllers/concerns/propel_controller_filters_concern.rb"
130
+ copy_file "concerns/propel_model_filters_concern.rb", "app/models/concerns/propel_model_filters_concern.rb"
131
+ copy_file "lib/propel_filter_operators.rb", "lib/propel_filter_operators.rb"
132
+ copy_file "lib/propel_dynamic_scope_generator.rb", "lib/propel_dynamic_scope_generator.rb"
133
+ say "šŸ“¦ Copied PropelApi filtering concerns to app", :green
134
+ say " - Controller filtering: app/controllers/concerns/propel_controller_filters_concern.rb", :blue
135
+ say " - Model filtering: app/models/concerns/propel_model_filters_concern.rb", :blue
136
+ say " - Filter operators: lib/propel_filter_operators.rb", :blue
137
+ say " - Dynamic scope generator: lib/propel_dynamic_scope_generator.rb", :blue
138
+ end
139
+ end
140
+
116
141
  def create_api_base_controller
117
142
  template "controllers/api_base_controller.rb", "app/controllers/api/base_controller.rb"
118
143
  say "Created Api::BaseController for CSRF-free API endpoints", :green
@@ -144,6 +169,18 @@ module PropelApi
144
169
  say "You can manually add the rescue_from handler if desired", :blue
145
170
  end
146
171
 
172
+ def add_model_filters_concern
173
+ # Inject PropelModelFiltersConcern into ApplicationRecord
174
+ inject_into_class "app/models/application_record.rb", ApplicationRecord, <<-RUBY
175
+ include PropelModelFiltersConcern
176
+ RUBY
177
+
178
+ say "Enhanced ApplicationRecord with dynamic filtering capabilities", :green
179
+ rescue => e
180
+ say "Could not inject PropelModelFiltersConcern into ApplicationRecord: #{e.message}", :yellow
181
+ say "You can manually add 'include PropelModelFiltersConcern' to ApplicationRecord if desired", :blue
182
+ end
183
+
147
184
  def copy_propel_facets_controller
148
185
  template "controllers/api_controller_propel_facets.rb", api_controller_path
149
186
  copy_example_controller
@@ -256,8 +293,12 @@ module PropelApi
256
293
  say " end", :yellow
257
294
  say "\n5. See doc/api_controller_example.rb for complete usage examples"
258
295
  say "6. Your API endpoints will be available at: #{api_route_prefix}/..."
259
- say "7. Pagination and filtering are built-in via Pagy and HasScope"
260
- say "\n8. Generate resources using:"
296
+ say "7. Pagination and secure filtering are built-in via Pagy and PropelApi filtering"
297
+ say "8. Filtering examples:"
298
+ say " GET /api/v1/users?name_contains=john&active_eq=true&sort=-created_at", :yellow
299
+ say " - Secure field validation prevents SQL injection"
300
+ say " - Sensitive fields (passwords, tokens) automatically excluded", :cyan
301
+ say "\n9. Generate resources using:"
261
302
  say " rails generate propel_api Resource field1:type field2:type", :yellow
262
303
  end
263
304
 
@@ -80,47 +80,11 @@ module PropelApi
80
80
  initialize_propel_api_settings
81
81
 
82
82
  if behavior == :revoke
83
- # Find the migration file
84
- migration_files = Dir[File.join(destination_root, "db/migrate/*_create_#{table_name}.rb")]
83
+ # Check for critical dependencies before destroying
84
+ check_for_critical_dependencies
85
85
 
86
- if migration_files.any?
87
- migration_file = migration_files.first
88
- migration_version = File.basename(migration_file).split('_').first
89
-
90
- # Check if migration was executed
91
- if migration_was_executed?(migration_version)
92
- say "\n" + "="*80, :red
93
- say "āš ļø MIGRATION ALREADY EXECUTED!", :red
94
- say "="*80, :red
95
- say "\nšŸ“‹ Migration #{migration_version} (create_#{table_name}) has been executed.", :yellow
96
- say "\n🚨 Rails does not automatically rollback migrations for safety reasons.", :yellow
97
- say "\nāœ… To safely destroy this resource:", :green
98
- say " 1. First rollback: rails db:rollback", :green
99
- say " 2. Then destroy: rails destroy propel_api #{class_name}", :green
100
- say "\nšŸ’” Alternative approaches:", :cyan
101
- say " • Check migration status: rails db:migrate:status"
102
- say " • Rollback specific: rails db:migrate:down VERSION=#{migration_version}"
103
- say "\n" + "="*80, :red
104
- raise Thor::Error, "Please rollback the migration first, then re-run destroy command."
105
- else
106
- # Migration not executed, check if safe to delete
107
- if safe_to_delete_migration?(migration_version)
108
- say "Removing migration: #{File.basename(migration_file)}", :red
109
- File.delete(migration_file)
110
- else
111
- say "\n" + "="*80, :yellow
112
- say "āš ļø MIGRATION CLEANUP REQUIRED", :yellow
113
- say "="*80, :yellow
114
- say "\nšŸ“‹ Found unexecuted migration: #{File.basename(migration_file)}", :cyan
115
- say "\n🚨 Cannot auto-delete due to intervening migrations.", :yellow
116
- say "šŸ’” Please manually remove when safe:", :green
117
- say " rm #{migration_file}"
118
- say "\n" + "="*80, :yellow
119
- end
120
- end
121
- else
122
- say "No migration file found for #{table_name}", :yellow
123
- end
86
+ # Generate proper removal migration following Rails conventions
87
+ generate_removal_migration
124
88
  else
125
89
  # Check for tenancy and warn if missing (unless warnings are disabled)
126
90
  check_tenancy_attributes_and_warn unless options[:skip_tenancy]
@@ -154,32 +118,49 @@ module PropelApi
154
118
  validate_attributes_exist
155
119
  end
156
120
 
157
- case @adapter
158
- when 'propel_facets'
159
- template "scaffold/facet_model_template.rb.tt", "app/models/#{file_name}.rb"
160
- when 'graphiti'
161
- template "scaffold/graphiti_model_template.rb.tt", "app/models/#{file_name}.rb"
162
- else
163
- raise "Unknown adapter: #{@adapter}. Run 'rails generate propel_api:install' first."
121
+ # Note: Relationship cleanup is now handled by the comprehensive dependency auto-fix system
122
+ # No need for separate inverse relationship logic here
123
+
124
+ # Direct template call - Rails can reverse this automatically (unless skipped)!
125
+ unless behavior == :revoke && options[:skip_model]
126
+ case @adapter
127
+ when 'propel_facets'
128
+ template "scaffold/facet_model_template.rb.tt", "app/models/#{file_name}.rb"
129
+ when 'graphiti'
130
+ template "scaffold/graphiti_model_template.rb.tt", "app/models/#{file_name}.rb"
131
+ else
132
+ raise "Unknown adapter: #{@adapter}. Run 'rails generate propel_api:install' first."
133
+ end
164
134
  end
165
135
 
166
- # Apply inverse relationships to existing parent models
167
- apply_inverse_relationships
136
+ # Apply inverse relationships AFTER template creation
137
+ if behavior != :revoke
138
+ apply_inverse_relationships
139
+ end
168
140
  end
169
141
 
170
142
  def create_controller
171
- # Use shared method from Base class
172
- create_propel_controller
143
+ create_propel_controller_template
173
144
  end
174
145
 
175
- def create_routes
176
- # Use shared method from Base class
146
+ def add_routes
177
147
  create_propel_routes
178
148
  end
179
149
 
180
- def create_tests
181
- # Use shared method from Base class with all test types
182
- create_propel_tests(test_types: [:model, :controller, :integration, :fixtures])
150
+ def create_model_test
151
+ create_propel_model_test unless behavior == :revoke && options[:skip_tests]
152
+ end
153
+
154
+ def create_controller_test
155
+ create_propel_controller_test unless behavior == :revoke && options[:skip_tests]
156
+ end
157
+
158
+ def create_integration_test
159
+ create_propel_integration_test unless behavior == :revoke && options[:skip_tests]
160
+ end
161
+
162
+ def create_fixtures
163
+ create_propel_fixtures unless behavior == :revoke && options[:skip_tests]
183
164
  end
184
165
 
185
166
  def create_seeds
@@ -288,13 +269,20 @@ module PropelApi
288
269
  next_steps << "Customize facets in: app/models/#{file_name}.rb"
289
270
  end
290
271
 
291
- show_propel_completion_message(
292
- resource_type: "Resource",
293
- generated_files: generated_files,
294
- next_steps: next_steps
295
- )
272
+ if behavior == :revoke
273
+ # Use shared destroy completion message from named_base
274
+ show_destroy_completion_message
275
+ else
276
+ show_propel_completion_message(
277
+ resource_type: "Resource",
278
+ generated_files: generated_files,
279
+ next_steps: next_steps
280
+ )
281
+ end
296
282
  end
297
283
 
284
+
285
+
298
286
  private
299
287
 
300
288
  def relationship_inferrer
@@ -419,6 +407,130 @@ module PropelApi
419
407
  end
420
408
  end
421
409
 
410
+ def remove_inverse_relationships
411
+ # Use Rails model reflection - much cleaner approach!
412
+ begin
413
+ model_class = class_name.constantize
414
+
415
+ # Get all belongs_to associations using Rails reflection
416
+ belongs_to_associations = model_class.reflect_on_all_associations(:belongs_to)
417
+
418
+ belongs_to_associations.each do |association|
419
+ # Skip polymorphic associations (they don't get inverse relationships)
420
+ next if association.polymorphic?
421
+
422
+ parent_class_name = association.class_name
423
+ parent_model_file = "app/models/#{parent_class_name.underscore}.rb"
424
+ full_path = File.join(destination_root, parent_model_file)
425
+
426
+ # Skip if parent model doesn't exist
427
+ next unless File.exist?(full_path)
428
+
429
+ # The inverse relationship we need to remove
430
+ inverse_relationship = "has_many :#{table_name}, dependent: :destroy"
431
+
432
+ begin
433
+ remove_specific_relationship(full_path, inverse_relationship, parent_class_name)
434
+ rescue => e
435
+ say "Warning: Could not remove relationship from #{parent_model_file}: #{e.message}", :yellow
436
+ end
437
+ end
438
+
439
+ rescue NameError => e
440
+ # Fallback: Parse attributes from migration file if model class doesn't exist
441
+ migration_attributes = get_attributes_from_migration
442
+ belongs_to_attributes = migration_attributes.select { |attr| attr[:type] == :references }
443
+
444
+ belongs_to_attributes.each do |attr|
445
+ # Skip polymorphic references
446
+ next if attr[:polymorphic]
447
+
448
+ parent_class_name = attr[:name].classify
449
+ parent_model_file = "app/models/#{parent_class_name.underscore}.rb"
450
+ full_path = File.join(destination_root, parent_model_file)
451
+
452
+ # Skip if parent model doesn't exist
453
+ next unless File.exist?(full_path)
454
+
455
+ inverse_relationship = "has_many :#{table_name}, dependent: :destroy"
456
+
457
+ begin
458
+ remove_specific_relationship(full_path, inverse_relationship, parent_class_name)
459
+ rescue => e
460
+ say "Warning: Could not remove relationship from #{parent_model_file}: #{e.message}", :yellow
461
+ end
462
+ end
463
+ end
464
+ end
465
+
466
+ def get_attributes_from_migration
467
+ # Find the migration file (using same pattern as create_migration)
468
+ migration_files = Dir[File.join(destination_root, "db/migrate/*_create_#{table_name}.rb")]
469
+ return [] unless migration_files.any?
470
+
471
+ migration_file = migration_files.first
472
+ migration_content = File.read(migration_file)
473
+
474
+ # Parse t.references and t.string, t.text, etc. lines
475
+ parsed_attributes = []
476
+
477
+ # Match references with polymorphic option
478
+ migration_content.scan(/t\.references\s+:(\w+).*?(polymorphic:\s*true)?/) do |name, polymorphic|
479
+ parsed_attributes << {
480
+ name: name,
481
+ type: :references,
482
+ polymorphic: !polymorphic.nil?
483
+ }
484
+ end
485
+
486
+ # Match other attribute types
487
+ %w[string text integer decimal boolean datetime date json jsonb].each do |type|
488
+ migration_content.scan(/t\.#{type}\s+:(\w+)/) do |name|
489
+ parsed_attributes << {
490
+ name: name.first,
491
+ type: type.to_sym,
492
+ polymorphic: false
493
+ }
494
+ end
495
+ end
496
+
497
+ parsed_attributes
498
+ end
499
+
500
+ def remove_specific_relationship(model_file_path, relationship, parent_class_name)
501
+ content = File.read(model_file_path)
502
+ original_content = content.dup
503
+
504
+ # Remove the relationship line if it exists
505
+ relationship_pattern = /^\s*#{Regexp.escape(relationship)}\s*\n/
506
+ if content.match?(relationship_pattern)
507
+ content = content.gsub(relationship_pattern, '')
508
+ File.write(model_file_path, content)
509
+ say " āŒ Removed from #{parent_class_name}: #{relationship}", :red
510
+ else
511
+ say " āš ļø Relationship not found in #{parent_class_name}: #{relationship}", :yellow
512
+ end
513
+ end
514
+
515
+ def remove_from_parent_model(model_file_path, relationships, parent_class_name)
516
+ content = File.read(model_file_path)
517
+ original_content = content.dup
518
+
519
+ relationships.each do |relationship|
520
+ # Remove the relationship line if it exists
521
+ relationship_pattern = /^\s*#{Regexp.escape(relationship)}\s*\n/
522
+ if content.match?(relationship_pattern)
523
+ content = content.gsub(relationship_pattern, '')
524
+ say " āŒ Removed from #{parent_class_name}: #{relationship}", :red
525
+ end
526
+ end
527
+
528
+ # Only write if content changed
529
+ if content != original_content
530
+ File.write(model_file_path, content)
531
+ end
532
+ end
533
+
422
534
  def update_parent_model(model_file_path, relationships, parent_class_name)
423
535
  content = File.read(model_file_path)
424
536
  original_content = content.dup
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DynamicControllerFiltersConcern
4
+ #
5
+ # Provides dynamic filtering capabilities for API controllers using has_scope.
6
+ # This concern automatically generates and registers scopes based on the model's
7
+ # database columns and their types.
8
+ #
9
+ # Features:
10
+ # - Automatic scope generation based on column types
11
+ # - Security validation of filter parameters
12
+ # - Error handling for malformed filters
13
+ # - Support for all standard filter operators
14
+ #
15
+ # Usage:
16
+ # class Api::V1::TeamsController < Api::V1::ApiController
17
+ # include PropelControllerFiltersConcern
18
+ # end
19
+ #
20
+ module PropelControllerFiltersConcern
21
+ extend ActiveSupport::Concern
22
+
23
+ included do
24
+ include HasScope
25
+
26
+ # Track which scopes have been registered to avoid duplicates
27
+ class_attribute :_registered_scopes, default: Set.new
28
+
29
+ # Register dynamic scopes when the controller is first used
30
+ before_action :ensure_dynamic_scopes_registered, prepend: true
31
+ end
32
+
33
+ private
34
+
35
+ def ensure_dynamic_scopes_registered
36
+ return if self.class._registered_scopes.include?(resource_class.name)
37
+
38
+ model_class = resource_class
39
+ return unless model_class&.respond_to?(:columns)
40
+
41
+ # Ensure scopes are generated for the model (lazy loading)
42
+ model_class.ensure_scopes_generated!
43
+
44
+ # Generate scopes for the model
45
+ generator = PropelDynamicScopeGenerator.new(model_class)
46
+ generator.generate_scopes!
47
+
48
+ # Register has_scope calls for this controller
49
+ generator.register_controller_scopes(self.class)
50
+
51
+ # Mark as registered to avoid duplicate work
52
+ self.class._registered_scopes << model_class.name
53
+ rescue StandardError => e
54
+ Rails.logger.error "Error registering dynamic scopes: #{e.message}"
55
+ Rails.logger.error e.backtrace.join("\n")
56
+ end
57
+
58
+ # Override apply_scopes to add security validation and error handling
59
+ def apply_scopes(base_scope)
60
+ # Filter out any potentially dangerous or unsupported parameters
61
+ safe_params = filter_safe_parameters(params)
62
+
63
+ # Apply scopes with error handling
64
+ result = super(base_scope, safe_params)
65
+ result || base_scope
66
+ rescue StandardError => e
67
+ Rails.logger.warn "Error in apply_scopes: #{e.message}"
68
+ base_scope
69
+ end
70
+
71
+ # Security: Filter out dangerous or unsupported parameters
72
+ def filter_safe_parameters(params)
73
+ return {} unless params.is_a?(ActionController::Parameters)
74
+
75
+ # Get allowed filter parameters based on model columns
76
+ allowed_filters = allowed_filter_parameters
77
+
78
+ # Filter to only include known, safe parameters
79
+ safe_params = params.slice(*allowed_filters)
80
+
81
+ # Log any filtered out parameters for security monitoring
82
+ filtered_params = params.except(*allowed_filters).except(:controller, :action, :format)
83
+ if filtered_params.present?
84
+ Rails.logger.warn "Filtered out potentially unsafe parameters: #{filtered_params.keys.join(', ')}"
85
+ end
86
+
87
+ safe_params
88
+ end
89
+
90
+ # Get list of allowed filter parameters based on model columns
91
+ def allowed_filter_parameters
92
+ return [] unless resource_class&.respond_to?(:columns)
93
+
94
+ allowed = []
95
+
96
+ resource_class.columns.each do |column|
97
+ name = column.name
98
+ type = column.type
99
+ sql_type = column.sql_type_metadata.sql_type
100
+
101
+ # Skip sensitive fields
102
+ next if sensitive_field?(name)
103
+
104
+ # Get operators for this column type
105
+ operators = PropelFilterOperators.operators_for(type, sql_type)
106
+
107
+ # Add parameter names for each operator
108
+ operators.each do |operator|
109
+ allowed << "#{name}_#{operator}".to_sym
110
+ end
111
+ end
112
+
113
+ # Add special parameters
114
+ allowed += [:order_by, :page, :limit, :per_page]
115
+
116
+ allowed
117
+ end
118
+
119
+ # Check if a field name is considered sensitive and should not be filterable
120
+ def sensitive_field?(field_name)
121
+ sensitive_fields = %w[
122
+ password password_hash password_digest
123
+ token secret_key api_key
124
+ encrypted_data encrypted_password
125
+ salt pepper
126
+ private_key public_key
127
+ ssn social_security_number
128
+ credit_card_number
129
+ bank_account_number
130
+ ]
131
+
132
+ sensitive_fields.any? { |sensitive| field_name.downcase.include?(sensitive) }
133
+ end
134
+
135
+ # Override to provide better error messages for invalid filters
136
+ def handle_invalid_filter(filter_name, error)
137
+ Rails.logger.warn "Invalid filter '#{filter_name}': #{error.message}"
138
+ # Don't raise the error, just log it and continue
139
+ # This prevents one bad filter from breaking the entire request
140
+ end
141
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DynamicModelFiltersConcern
4
+ #
5
+ # Provides dynamic filtering capabilities for ActiveRecord models.
6
+ # This concern automatically generates scopes based on the model's
7
+ # database columns and their types.
8
+ #
9
+ # Features:
10
+ # - Automatic scope generation based on column types
11
+ # - Support for all standard filter operators
12
+ # - Type-aware filtering (boolean, string, numeric, datetime)
13
+ # - Security validation of filter parameters
14
+ #
15
+ # Usage:
16
+ # class Team < ApplicationRecord
17
+ # include PropelModelFiltersConcern
18
+ # end
19
+ #
20
+ module PropelModelFiltersConcern
21
+ extend ActiveSupport::Concern
22
+
23
+ included do
24
+ # Track which scopes have been generated to avoid duplicates
25
+ class_attribute :_generated_scopes, default: Set.new
26
+
27
+ # Generate scopes lazily when first needed
28
+ # This prevents issues with abstract classes like ApplicationRecord
29
+ end
30
+
31
+ class_methods do
32
+ # Generate all dynamic scopes for this model
33
+ def generate_dynamic_scopes!
34
+ return if _generated_scopes.include?(name)
35
+
36
+ # Skip abstract classes (like ApplicationRecord)
37
+ return if abstract_class?
38
+
39
+ # Skip if the model doesn't have a table (e.g., abstract classes)
40
+ return unless table_exists?
41
+
42
+ generator = PropelDynamicScopeGenerator.new(self)
43
+ generator.generate_scopes!
44
+
45
+ # Mark as generated to avoid duplicate work
46
+ _generated_scopes << name
47
+ rescue StandardError => e
48
+ Rails.logger.error "Error generating dynamic scopes for #{name}: #{e.message}"
49
+ Rails.logger.error e.backtrace.join("\n")
50
+ end
51
+
52
+ # Ensure scopes are generated (lazy loading)
53
+ def ensure_scopes_generated!
54
+ generate_dynamic_scopes! unless _generated_scopes.include?(name)
55
+ end
56
+
57
+ # Get all available filter parameters for this model
58
+ def available_filter_parameters
59
+ return [] unless respond_to?(:columns)
60
+
61
+ allowed = []
62
+
63
+ columns.each do |column|
64
+ name = column.name
65
+ type = column.type
66
+ sql_type = column.sql_type_metadata.sql_type
67
+
68
+ # Skip sensitive fields
69
+ next if sensitive_field?(name)
70
+
71
+ # Get operators for this column type
72
+ operators = PropelFilterOperators.operators_for(type, sql_type)
73
+
74
+ # Add parameter names for each operator
75
+ operators.each do |operator|
76
+ allowed << "#{name}_#{operator}".to_sym
77
+ end
78
+ end
79
+
80
+ allowed
81
+ end
82
+
83
+ # Check if a field name is considered sensitive
84
+ def sensitive_field?(field_name)
85
+ sensitive_fields = %w[
86
+ password password_hash password_digest
87
+ token secret_key api_key
88
+ encrypted_data encrypted_password
89
+ salt pepper
90
+ private_key public_key
91
+ ssn social_security_number
92
+ credit_card_number
93
+ bank_account_number
94
+ ]
95
+
96
+ sensitive_fields.any? { |sensitive| field_name.downcase.include?(sensitive) }
97
+ end
98
+
99
+ # Get filterable columns for this model
100
+ def filterable_columns
101
+ return [] unless respond_to?(:columns)
102
+
103
+ columns.reject { |column| sensitive_field?(column.name) }
104
+ end
105
+
106
+ # Get operators available for a specific column
107
+ def operators_for_column(column_name)
108
+ column = columns.find { |c| c.name == column_name }
109
+ return [] unless column
110
+
111
+ PropelFilterOperators.operators_for(column.type, column.sql_type_metadata.sql_type)
112
+ end
113
+
114
+ # Validate filter parameters
115
+ def validate_filter_parameters(params)
116
+ return {} unless params.is_a?(ActionController::Parameters)
117
+
118
+ allowed_filters = available_filter_parameters
119
+ params.slice(*allowed_filters)
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def ensure_dynamic_scopes_generated
126
+ self.class.generate_dynamic_scopes!
127
+ end
128
+ end
@@ -4,6 +4,7 @@ class <%= api_controller_class_name %> < Api::BaseController
4
4
  include FacetRenderer
5
5
  include StrongParamsHelper
6
6
  include PropelAuthenticationConcern
7
+ include PropelControllerFiltersConcern
7
8
 
8
9
  before_action :authenticate_user
9
10
  before_action :set_resource, only: [:show, :update, :destroy]