securial 0.4.2 → 0.6.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -14
  3. data/app/controllers/concerns/securial/identity.rb +4 -12
  4. data/app/controllers/securial/sessions_controller.rb +20 -8
  5. data/app/models/concerns/securial/password_resettable.rb +15 -0
  6. data/app/models/securial/session.rb +2 -2
  7. data/app/views/securial/roles/_securial_role.json.jbuilder +1 -2
  8. data/app/views/securial/sessions/_session.json.jbuilder +1 -2
  9. data/app/views/securial/shared/_timestamps.json.jbuilder +8 -0
  10. data/app/views/securial/users/_securial_user.json.jbuilder +3 -2
  11. data/config/routes.rb +0 -1
  12. data/db/migrate/20250517155521_create_securial_users.rb +10 -7
  13. data/lib/generators/securial/install/templates/securial_initializer.erb +53 -12
  14. data/lib/generators/securial/install/views_generastor.rb +25 -0
  15. data/lib/generators/securial/jbuilder/templates/_resource.json.erb +1 -2
  16. data/lib/securial/auth/_index.rb +3 -0
  17. data/lib/securial/auth/auth_encoder.rb +56 -0
  18. data/lib/securial/auth/errors.rb +15 -0
  19. data/lib/securial/auth/session_creator.rb +21 -0
  20. data/lib/securial/config/_index.rb +3 -0
  21. data/lib/securial/config/configuration.rb +62 -0
  22. data/lib/securial/config/errors.rb +20 -0
  23. data/lib/securial/config/validation.rb +249 -0
  24. data/lib/securial/engine.rb +41 -38
  25. data/lib/securial/helpers/_index.rb +2 -0
  26. data/lib/securial/helpers/regex_helper.rb +7 -7
  27. data/lib/securial/inspectors/_index.rb +1 -0
  28. data/lib/securial/inspectors/route_inspector.rb +52 -0
  29. data/lib/securial/key_transformer.rb +32 -0
  30. data/lib/securial/logger.rb +2 -2
  31. data/lib/securial/middleware/_index.rb +3 -0
  32. data/lib/securial/middleware/transform_request_keys.rb +33 -0
  33. data/lib/securial/middleware/transform_response_keys.rb +45 -0
  34. data/lib/securial/rack_attack.rb +48 -0
  35. data/lib/securial/version.rb +1 -1
  36. data/lib/securial.rb +7 -72
  37. metadata +37 -154
  38. data/app/views/securial/passwords/_password.json.jbuilder +0 -2
  39. data/app/views/securial/passwords/index.json.jbuilder +0 -1
  40. data/app/views/securial/passwords/show.json.jbuilder +0 -1
  41. data/db/migrate/20250524210207_add_password_reset_fields_to_securial_users.rb +0 -6
  42. data/lib/securial/configuration.rb +0 -35
  43. data/lib/securial/errors/config_errors.rb +0 -12
  44. data/lib/securial/errors/session_errors.rb +0 -6
  45. data/lib/securial/helpers/auth_helper.rb +0 -46
  46. data/lib/securial/route_inspector.rb +0 -50
@@ -0,0 +1,249 @@
1
+ require "securial/logger"
2
+
3
+ module Securial
4
+ module Config
5
+ module Validation
6
+ class << self # rubocop:disable Metrics/ClassLength
7
+ def validate_all!(config)
8
+ validate_admin_role!(config)
9
+ validate_session_config!(config)
10
+ validate_mailer_sender!(config)
11
+ validate_password_config!(config)
12
+ validate_response_config!(config)
13
+ validate_security_config!(config)
14
+ end
15
+
16
+ private
17
+
18
+ def validate_admin_role!(config)
19
+ if config.admin_role.nil? || config.admin_role.to_s.strip.empty?
20
+ error_message = "Admin role is not set."
21
+ Securial::ENGINE_LOGGER.fatal(error_message)
22
+ raise Securial::Config::Errors::ConfigAdminRoleError, error_message
23
+ end
24
+
25
+ unless config.admin_role.is_a?(Symbol) || config.admin_role.is_a?(String)
26
+ error_message = "Admin role must be a Symbol or String."
27
+ Securial::ENGINE_LOGGER.fatal(error_message)
28
+ raise Securial::Config::Errors::ConfigAdminRoleError, error_message
29
+ end
30
+
31
+ if config.admin_role.to_s.pluralize.downcase == "accounts"
32
+ error_message = "The admin role cannot be 'account' or 'accounts' as it conflicts with the default routes."
33
+ Securial::ENGINE_LOGGER.fatal(error_message)
34
+ raise Securial::Config::Errors::ConfigAdminRoleError, error_message
35
+ end
36
+ end
37
+
38
+ def validate_session_config!(config)
39
+ validate_session_expiry_duration!(config)
40
+ validate_session_algorithm!(config)
41
+ validate_session_secret!(config)
42
+ end
43
+
44
+ def validate_session_expiry_duration!(config)
45
+ if config.session_expiration_duration.nil?
46
+ error_message = "Session expiration duration is not set."
47
+ Securial::ENGINE_LOGGER.fatal(error_message)
48
+ raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
49
+ end
50
+ if config.session_expiration_duration.class != ActiveSupport::Duration
51
+ error_message = "Session expiration duration must be an ActiveSupport::Duration."
52
+ Securial::ENGINE_LOGGER.fatal(error_message)
53
+ raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
54
+ end
55
+ if config.session_expiration_duration <= 0
56
+ Securial::ENGINE_LOGGER.fatal("Session expiration duration must be greater than 0.")
57
+ raise Securial::Config::Errors::ConfigSessionExpirationDurationError, "Session expiration duration must be greater than 0."
58
+ end
59
+ end
60
+
61
+ def validate_session_algorithm!(config)
62
+ if config.session_algorithm.blank?
63
+ error_message = "Session algorithm is not set."
64
+ Securial::ENGINE_LOGGER.fatal(error_message)
65
+ raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
66
+ end
67
+ unless config.session_algorithm.is_a?(Symbol)
68
+ error_message = "Session algorithm must be a Symbol."
69
+ Securial::ENGINE_LOGGER.fatal(error_message)
70
+ raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
71
+ end
72
+ valid_algorithms = Securial::Config::VALID_SESSION_ENCRYPTION_ALGORITHMS
73
+ unless valid_algorithms.include?(config.session_algorithm)
74
+ error_message = "Invalid session algorithm. Valid options are: #{valid_algorithms.map(&:inspect).join(', ')}."
75
+ Securial::ENGINE_LOGGER.fatal(error_message)
76
+ raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
77
+ end
78
+ end
79
+
80
+ def validate_session_secret!(config)
81
+ if config.session_secret.blank?
82
+ error_message = "Session secret is not set."
83
+ Securial::ENGINE_LOGGER.fatal(error_message)
84
+ raise Securial::Config::Errors::ConfigSessionSecretError, error_message
85
+ end
86
+ unless config.session_secret.is_a?(String)
87
+ error_message = "Session secret must be a String."
88
+ Securial::ENGINE_LOGGER.fatal(error_message)
89
+ raise Securial::Config::Errors::ConfigSessionSecretError, error_message
90
+ end
91
+ end
92
+
93
+ def validate_mailer_sender!(config)
94
+ if config.mailer_sender.blank?
95
+ error_message = "Mailer sender is not set."
96
+ Securial::ENGINE_LOGGER.fatal(error_message)
97
+ raise Securial::Config::Errors::ConfigMailerSenderError, error_message
98
+ end
99
+ if config.mailer_sender !~ URI::MailTo::EMAIL_REGEXP
100
+ error_message = "Mailer sender is not a valid email address."
101
+ Securial::ENGINE_LOGGER.fatal(error_message)
102
+ raise Securial::Config::Errors::ConfigMailerSenderError, error_message
103
+ end
104
+ end
105
+
106
+ def validate_password_config!(config)
107
+ validate_password_reset_subject!(config)
108
+ validate_password_min_max_length!(config)
109
+ validate_password_complexity!(config)
110
+ validate_password_expiration!(config)
111
+ validate_password_reset_token!(config)
112
+ end
113
+
114
+ def validate_password_reset_token!(config)
115
+ if config.reset_password_token_secret.blank?
116
+ error_message = "Reset password token secret is not set."
117
+ Securial::ENGINE_LOGGER.fatal(error_message)
118
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
119
+ end
120
+ unless config.reset_password_token_secret.is_a?(String)
121
+ error_message = "Reset password token secret must be a String."
122
+ Securial::ENGINE_LOGGER.fatal(error_message)
123
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
124
+ end
125
+ end
126
+
127
+ def validate_password_reset_subject!(config)
128
+ if config.password_reset_email_subject.blank?
129
+ error_message = "Password reset email subject is not set."
130
+ Securial::ENGINE_LOGGER.fatal(error_message)
131
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
132
+ end
133
+ unless config.password_reset_email_subject.is_a?(String)
134
+ error_message = "Password reset email subject must be a String."
135
+ Securial::ENGINE_LOGGER.fatal(error_message)
136
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
137
+ end
138
+ end
139
+
140
+ def validate_password_min_max_length!(config)
141
+ unless config.password_min_length.is_a?(Integer) && config.password_min_length > 0
142
+ error_message = "Password minimum length must be a positive integer."
143
+ Securial::ENGINE_LOGGER.fatal(error_message)
144
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
145
+ end
146
+ unless config.password_max_length.is_a?(Integer) && config.password_max_length >= config.password_min_length
147
+ error_message = "Password maximum length must be an integer greater than or equal to the minimum length."
148
+ Securial::ENGINE_LOGGER.fatal(error_message)
149
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
150
+ end
151
+ end
152
+
153
+ def validate_password_complexity!(config)
154
+ if config.password_complexity.nil? || !config.password_complexity.is_a?(Regexp)
155
+ error_message = "Password complexity regex is not set or is not a valid Regexp."
156
+ Securial::ENGINE_LOGGER.fatal(error_message)
157
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
158
+ end
159
+ end
160
+
161
+ def validate_password_expiration!(config)
162
+ unless config.password_expires.is_a?(TrueClass) || config.password_expires.is_a?(FalseClass)
163
+ error_message = "Password expiration must be a boolean value."
164
+ Securial::ENGINE_LOGGER.fatal(error_message)
165
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
166
+ end
167
+
168
+ if config.password_expires == true && (
169
+ config.password_expires_in.nil? ||
170
+ !config.password_expires_in.is_a?(ActiveSupport::Duration) ||
171
+ config.password_expires_in <= 0
172
+ )
173
+ error_message = "Password expiration duration is not set or is not a valid ActiveSupport::Duration."
174
+ Securial::ENGINE_LOGGER.fatal(error_message)
175
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
176
+ end
177
+ end
178
+
179
+ def validate_response_config!(config)
180
+ validate_response_keys_format!(config)
181
+ validate_timestamps_in_response!(config)
182
+ end
183
+
184
+ def validate_response_keys_format!(config)
185
+ valid_formats = Securial::Config::VALID_RESPONSE_KEYS_FORMATS
186
+ unless valid_formats.include?(config.response_keys_format)
187
+ error_message = "Invalid response_keys_format option. Valid options are: #{valid_formats.map(&:inspect).join(', ')}."
188
+ Securial::ENGINE_LOGGER.fatal(error_message)
189
+ raise Securial::Config::Errors::ConfigResponseError, error_message
190
+ end
191
+ end
192
+
193
+ def validate_timestamps_in_response!(config)
194
+ valid_options = Securial::Config::VALID_TIMESTAMP_OPTIONS
195
+ unless valid_options.include?(config.timestamps_in_response)
196
+ error_message = "Invalid timestamps_in_response option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
197
+ Securial::ENGINE_LOGGER.fatal(error_message)
198
+ raise Securial::Config::Errors::ConfigResponseError, error_message
199
+ end
200
+ end
201
+
202
+ def validate_security_config!(config)
203
+ validate_security_headers!(config)
204
+ validate_rate_limiting!(config)
205
+ end
206
+
207
+ def validate_security_headers!(config)
208
+ valid_options = Securial::Config::VALID_SECURITY_HEADERS
209
+ unless valid_options.include?(config.security_headers)
210
+ error_message = "Invalid security_headers option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
211
+ Securial::ENGINE_LOGGER.fatal(error_message)
212
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
213
+ end
214
+ end
215
+
216
+ def validate_rate_limiting!(config) # rubocop:disable Metrics/MethodLength
217
+ unless config.rate_limiting_enabled.is_a?(TrueClass) || config.rate_limiting_enabled.is_a?(FalseClass)
218
+ error_message = "rate_limiting_enabled must be a boolean value."
219
+ Securial::ENGINE_LOGGER.fatal(error_message)
220
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
221
+ end
222
+
223
+ return unless config.rate_limiting_enabled
224
+
225
+ unless
226
+ config.rate_limit_requests_per_minute.is_a?(Integer) &&
227
+ config.rate_limit_requests_per_minute > 0
228
+
229
+ error_message = "rate_limit_requests_per_minute must be a positive integer when rate limiting is enabled."
230
+ Securial::ENGINE_LOGGER.fatal(error_message)
231
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
232
+ end
233
+
234
+ unless config.rate_limit_response_status.is_a?(Integer) && config.rate_limit_response_status.between?(400, 599)
235
+ error_message = "rate_limit_response_status must be an HTTP status code between 4xx and 5xx."
236
+ Securial::ENGINE_LOGGER.fatal(error_message)
237
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
238
+ end
239
+
240
+ unless config.rate_limit_response_message.is_a?(String) && !config.rate_limit_response_message.strip.empty?
241
+ error_message = "rate_limit_response_message must be a non-empty String."
242
+ Securial::ENGINE_LOGGER.fatal(error_message)
243
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -1,22 +1,22 @@
1
1
  require_relative "./logger"
2
+ require_relative "./key_transformer"
3
+ require_relative "./config/_index"
4
+ require_relative "./helpers/_index"
5
+ require_relative "./auth/_index"
6
+ require_relative "./inspectors/_index"
2
7
 
3
- Dir[File.join(__dir__, "errors", "*.rb")].each { |f| require_relative f }
4
-
5
- require_relative "./configuration"
6
-
7
- require_relative "./helpers/auth_helper"
8
- require_relative "./helpers/normalizing_helper"
9
- require_relative "./helpers/regex_helper"
10
-
11
- require_relative "./route_inspector"
12
-
13
- require_relative "./middleware/request_logger_tag"
8
+ require_relative "./middleware/_index"
14
9
  require "jwt"
15
10
 
16
11
  module Securial
17
12
  class Engine < ::Rails::Engine
18
13
  isolate_namespace Securial
19
14
 
15
+ # Set API-only mode and autoload custom generator paths
16
+ config.api_only = true
17
+ config.generators.api_only = true
18
+ config.autoload_paths += Dir["#{config.root}/lib/generators"]
19
+
20
20
  initializer "securial.filter_parameters" do |app|
21
21
  app.config.filter_parameters += [
22
22
  :password,
@@ -30,37 +30,47 @@ module Securial
30
30
  Securial.const_set(:ENGINE_LOGGER, Securial::Logger.build)
31
31
  end
32
32
 
33
- initializer "securial.engine_initialized" do |app|
34
- Securial::ENGINE_LOGGER.info("[Securial] Engine mounted. Host app: #{app.class.name}")
33
+ initializer "securial.log_initialization" do |app|
34
+ log "[Initializing Engine] Host app: #{app.class.name}"
35
35
  end
36
36
 
37
- initializer "securial.factories", after: "factory_bot.set_factory_paths" do
38
- if defined?(FactoryBot)
39
- FactoryBot.definition_file_paths << Engine.root.join("lib", "securial", "factories")
40
- end
37
+ initializer "securial.validate_config" do
38
+ log "[Validating configuration] from `config/initializers/securial.rb`..."
39
+ Securial::Config::Validation.validate_all!(Securial.configuration)
40
+ end
41
+
42
+ initializer "securial.extend_application_controller" do
43
+ ActiveSupport.on_load(:action_controller_base) { include Securial::Identity }
44
+ ActiveSupport.on_load(:action_controller_api) { include Securial::Identity }
41
45
  end
42
46
 
43
- initializer "securial.load_factory_bot_generator" do
47
+ initializer "securial.factory_bot", after: "factory_bot.set_factory_paths" do
48
+ FactoryBot.definition_file_paths << Engine.root.join("lib", "securial", "factories") if defined?(FactoryBot)
49
+ end
50
+
51
+ initializer "securial.factory_bot_generator" do
44
52
  require_relative "../generators/factory_bot/model/model_generator"
45
53
  end
46
54
 
47
- initializer "securial.extend_application_controller" do
48
- ActiveSupport.on_load(:action_controller_base) do
49
- include Securial::Identity
55
+ initializer "securial.middleware" do |app|
56
+ middleware.use Securial::Middleware::RequestLoggerTag
57
+ middleware.use Securial::Middleware::TransformRequestKeys
58
+ middleware.use Securial::Middleware::TransformResponseKeys
59
+ if Securial.configuration.rate_limiting_enabled
60
+ require "rack/attack"
61
+ require_relative "./rack_attack"
62
+ middleware.use Rack::Attack
50
63
  end
64
+ end
51
65
 
52
- ActiveSupport.on_load(:action_controller_api) do
53
- include Securial::Identity
66
+ initializer "securial.log_ready", after: :load_config_initializers do
67
+ Rails.application.config.after_initialize do
68
+ log "[Engine fully initialized] Environment: #{Rails.env}"
54
69
  end
55
70
  end
56
71
 
57
- config.generators.api_only = true
58
-
59
- config.autoload_paths += Dir["#{config.root}/lib/generators"]
60
-
61
72
  config.generators do |g|
62
73
  g.orm :active_record, primary_key_type: :string
63
-
64
74
  g.test_framework :rspec,
65
75
  fixtures: false,
66
76
  view_specs: false,
@@ -69,21 +79,14 @@ module Securial
69
79
  controller_specs: true,
70
80
  request_specs: true,
71
81
  model_specs: true
72
-
73
82
  g.fixture_replacement :factory_bot, dir: "lib/securial/factories"
74
-
75
- # Add JBuilder configuration
76
83
  g.template_engine :jbuilder
77
84
  end
78
85
 
79
- initializer "securial.middleware" do |app|
80
- app.middleware.use Securial::Middleware::RequestLoggerTag
81
- end
86
+ private
82
87
 
83
- initializer "securial.log_engine_loaded", after: :load_config_initializers do
84
- Rails.application.config.after_initialize do
85
- Securial::ENGINE_LOGGER.info("[Securial] Engine fully initialized in #{Rails.env} environment.")
86
- end
88
+ def log(message)
89
+ Securial::ENGINE_LOGGER.info("[Securial] #{message}")
87
90
  end
88
91
  end
89
92
  end
@@ -0,0 +1,2 @@
1
+ require_relative "normalizing_helper"
2
+ require_relative "regex_helper"
@@ -4,14 +4,14 @@ module Securial
4
4
  USERNAME_REGEX = /\A(?![0-9])[a-zA-Z](?:[a-zA-Z0-9]|[._](?![._]))*[a-zA-Z0-9]\z/
5
5
  PASSWORD_REGEX = %r{\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])[a-zA-Z].*\z}
6
6
 
7
- module_function
7
+ class << self
8
+ def valid_email?(email)
9
+ email.match?(EMAIL_REGEX)
10
+ end
8
11
 
9
- def valid_email?(email)
10
- email.match?(EMAIL_REGEX)
11
- end
12
-
13
- def valid_username?(username)
14
- username.match?(USERNAME_REGEX)
12
+ def valid_username?(username)
13
+ username.match?(USERNAME_REGEX)
14
+ end
15
15
  end
16
16
  end
17
17
  end
@@ -0,0 +1 @@
1
+ require_relative "route_inspector"
@@ -0,0 +1,52 @@
1
+ module Securial
2
+ module Inspectors
3
+ module RouteInspector
4
+ def self.print_routes(controller: nil)
5
+ filtered = Securial::Engine.routes.routes.select do |r|
6
+ ctrl = r.defaults[:controller]
7
+ controller.nil? || ctrl == "securial/#{controller}"
8
+ end
9
+
10
+ print_headers(filtered, controller)
11
+ print_details(filtered, controller)
12
+ true
13
+ end
14
+
15
+ class << self
16
+ private
17
+
18
+ # rubocop:disable Rails/Output
19
+ def print_headers(filtered, controller)
20
+ Securial::ENGINE_LOGGER.debug "Securial engine routes:"
21
+ Securial::ENGINE_LOGGER.debug "Total routes: #{filtered.size}"
22
+ Securial::ENGINE_LOGGER.debug "Filtered by controller: #{controller}" if controller
23
+ Securial::ENGINE_LOGGER.debug "Filtered routes: #{filtered.size}" if controller
24
+ Securial::ENGINE_LOGGER.debug "-" * 120
25
+ Securial::ENGINE_LOGGER.debug "#{'Verb'.ljust(8)} #{'Path'.ljust(45)} #{'Controller#Action'.ljust(40)} Name"
26
+ Securial::ENGINE_LOGGER.debug "-" * 120
27
+ end
28
+
29
+ def print_details(filtered, controller) # rubocop:disable Rails/Output
30
+ if filtered.empty?
31
+ if controller
32
+ Securial::ENGINE_LOGGER.debug "No routes found for controller: #{controller}"
33
+ else
34
+ Securial::ENGINE_LOGGER.debug "No routes found for Securial engine"
35
+ end
36
+ Securial::ENGINE_LOGGER.debug "-" * 120
37
+ return
38
+ end
39
+
40
+ Securial::ENGINE_LOGGER.debug filtered.map { |r|
41
+ name = r.name || ""
42
+ verb = r.verb.to_s.ljust(8)
43
+ path = r.path.spec.to_s.sub(/\(\.:format\)/, "").ljust(45)
44
+ ctrl_action = "#{r.defaults[:controller]}##{r.defaults[:action]}"
45
+ "#{verb} #{path} #{ctrl_action.ljust(40)} #{name}"
46
+ }.join("\n")
47
+ end
48
+ # rubocop:enable Rails/Output
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,32 @@
1
+ # lib/securial/key_transformer.rb
2
+ module Securial
3
+ module KeyTransformer
4
+ def self.camelize(str, format)
5
+ return str unless str.is_a?(String)
6
+
7
+ case format
8
+ when :lowerCamelCase
9
+ str.camelize(:lower)
10
+ when :UpperCamelCase
11
+ str.camelize
12
+ else
13
+ str
14
+ end
15
+ end
16
+
17
+ def self.underscore(str)
18
+ str.to_s.underscore
19
+ end
20
+
21
+ def self.deep_transform_keys(obj, &block)
22
+ case obj
23
+ when Hash
24
+ obj.transform_keys(&block).transform_values { |v| deep_transform_keys(v, &block) }
25
+ when Array
26
+ obj.map { |e| deep_transform_keys(e, &block) }
27
+ else
28
+ obj
29
+ end
30
+ end
31
+ end
32
+ end
@@ -7,13 +7,13 @@ module Securial
7
7
  def self.build
8
8
  outputs = []
9
9
 
10
- if Securial.configuration.log_to_file
10
+ unless Securial.configuration.log_to_file == false
11
11
  log_file = Rails.root.join("log", "securial.log").open("a")
12
12
  log_file.sync = true
13
13
  outputs << log_file
14
14
  end
15
15
 
16
- if Securial.configuration.log_to_stdout
16
+ unless Securial.configuration.log_to_stdout == false
17
17
  outputs << STDOUT
18
18
  end
19
19
 
@@ -0,0 +1,3 @@
1
+ require_relative "./transform_request_keys"
2
+ require_relative "./transform_response_keys"
3
+ require_relative "./request_logger_tag"
@@ -0,0 +1,33 @@
1
+ # lib/securial/middleware/transform_request_keys.rb
2
+ require "json"
3
+
4
+ module Securial
5
+ module Middleware
6
+ class TransformRequestKeys
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ if env["CONTENT_TYPE"]&.include?("application/json") && Securial.configuration.response_keys_format != :snake_case
13
+ req = Rack::Request.new(env)
14
+ if (req.body&.size || 0) > 0
15
+ raw = req.body.read
16
+ req.body.rewind
17
+ begin
18
+ parsed = JSON.parse(raw)
19
+ transformed = Securial::KeyTransformer.deep_transform_keys(parsed) do |key|
20
+ Securial::KeyTransformer.underscore(key)
21
+ end
22
+ env["rack.input"] = StringIO.new(JSON.dump(transformed))
23
+ env["rack.input"].rewind
24
+ rescue JSON::ParserError
25
+ # noop
26
+ end
27
+ end
28
+ end
29
+ @app.call(env)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ # lib/securial/middleware/transform_response_keys.rb
2
+ require "json"
3
+
4
+ module Securial
5
+ module Middleware
6
+ class TransformResponseKeys
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ status, headers, response = @app.call(env)
13
+
14
+ if json_response?(headers)
15
+ body = extract_body(response)
16
+
17
+ if body.present?
18
+ format = Securial.configuration.response_keys_format
19
+ transformed = KeyTransformer.deep_transform_keys(JSON.parse(body)) do |key|
20
+ KeyTransformer.camelize(key, format)
21
+ end
22
+
23
+ new_body = [JSON.generate(transformed)]
24
+ headers["Content-Length"] = new_body.first.bytesize.to_s
25
+ return [status, headers, new_body]
26
+ end
27
+ end
28
+
29
+ [status, headers, response]
30
+ end
31
+
32
+ private
33
+
34
+ def json_response?(headers)
35
+ headers["Content-Type"]&.include?("application/json")
36
+ end
37
+
38
+ def extract_body(response)
39
+ response_body = ""
40
+ response.each { |part| response_body << part }
41
+ response_body
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ Rails.application.config.to_prepare do
2
+ class Rack::Attack
3
+ limit = Securial.configuration.rate_limit_requests_per_minute
4
+ resp_status = Securial.configuration.rate_limit_response_status
5
+ resp_message = Securial.configuration.rate_limit_response_message
6
+
7
+ ### Throttle login attempts by IP ###
8
+ throttle("securial/logins/ip", limit: limit, period: 1.minute) do |req|
9
+ if req.path.include?("sessions/login") && req.post?
10
+ req.ip
11
+ end
12
+ end
13
+
14
+ ### Throttle login attempts by username ###
15
+ throttle("securial/logins/email", limit: 5, period: 1.minute) do |req|
16
+ if req.path.include?("sessions/login") && req.post?
17
+ req.params["email_address"].to_s.downcase.strip
18
+ end
19
+ end
20
+
21
+ ### TODO: Add throttling for other endpoints as needed ###
22
+ # Example: Throttle password reset requests by email
23
+ throttle("securial/password_resets/ip", limit: 5, period: 1.minute) do |req|
24
+ if req.path.include?("password/forgot") && req.post?
25
+ req.ip
26
+ end
27
+ end
28
+
29
+ throttle("securial/password_resets/email", limit: 5, period: 1.minute) do |req|
30
+ if req.path.include?("password/forgot") && req.post?
31
+ req.params["email_address"].to_s.downcase.strip
32
+ end
33
+ end
34
+
35
+ ### Custom response ###
36
+ self.throttled_responder = lambda do |request|
37
+ retry_after = (request.env["rack.attack.match_data"] || {})[:period]
38
+ [
39
+ resp_status,
40
+ {
41
+ "Content-Type" => "application/json",
42
+ "Retry-After" => retry_after.to_s,
43
+ },
44
+ [{ error: resp_message }.to_json]
45
+ ]
46
+ end
47
+ end
48
+ end
@@ -1,3 +1,3 @@
1
1
  module Securial
2
- VERSION = "0.4.2"
2
+ VERSION = "0.6.1".freeze
3
3
  end