securial 0.6.1 → 0.7.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/securial/status_controller.rb +1 -1
  3. data/app/models/securial/role.rb +1 -1
  4. data/app/models/securial/user.rb +3 -3
  5. data/lib/generators/securial/install/templates/securial_initializer.erb +1 -1
  6. data/lib/securial/auth/auth_encoder.rb +39 -42
  7. data/lib/securial/auth/session_creator.rb +12 -12
  8. data/lib/securial/auth.rb +10 -0
  9. data/lib/securial/config/configuration.rb +3 -3
  10. data/lib/securial/config/validation.rb +29 -29
  11. data/lib/securial/config.rb +10 -0
  12. data/lib/securial/engine.rb +26 -15
  13. data/lib/securial/helpers/normalizing_helper.rb +11 -9
  14. data/lib/securial/helpers/regex_helper.rb +12 -10
  15. data/lib/securial/helpers.rb +9 -0
  16. data/lib/securial/inspectors/route_inspector.rb +11 -11
  17. data/lib/securial/inspectors.rb +8 -0
  18. data/lib/securial/logger/broadcaster.rb +48 -0
  19. data/lib/securial/logger/builder.rb +60 -0
  20. data/lib/securial/logger/colors.rb +14 -0
  21. data/lib/securial/logger.rb +4 -67
  22. data/lib/securial/middlewares/request_logger_tag.rb +19 -0
  23. data/lib/securial/{middleware → middlewares}/transform_request_keys.rb +2 -2
  24. data/lib/securial/{middleware → middlewares}/transform_response_keys.rb +15 -8
  25. data/lib/securial/middlewares.rb +10 -0
  26. data/lib/securial/security/request_rate_limiter.rb +68 -0
  27. data/lib/securial/security.rb +8 -0
  28. data/lib/securial/version.rb +1 -1
  29. data/lib/securial/version_checker.rb +31 -0
  30. data/lib/securial.rb +6 -0
  31. metadata +18 -13
  32. data/lib/securial/auth/_index.rb +0 -3
  33. data/lib/securial/config/_index.rb +0 -3
  34. data/lib/securial/helpers/_index.rb +0 -2
  35. data/lib/securial/inspectors/_index.rb +0 -1
  36. data/lib/securial/middleware/_index.rb +0 -3
  37. data/lib/securial/middleware/request_logger_tag.rb +0 -18
  38. data/lib/securial/rack_attack.rb +0 -48
  39. /data/lib/generators/securial/install/{views_generastor.rb → views_generator.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1628d056fdb0a93bc954107766c8f5ed2975d4690c58dbdbe56a0d45e63eefa3
4
- data.tar.gz: 166ce5d80dc63642239a0f6c26702e4bbca469c2f7d886d322aa07e119ef8928
3
+ metadata.gz: 6955f517ae29e92b5a870f5e951782374d39f7c5eeb19de024d818106a57cc82
4
+ data.tar.gz: a91673febf7143ab6b245feab4154d1d511ca62b7f78f6a94c758b974c2860ed
5
5
  SHA512:
6
- metadata.gz: 997ba8dc253bf3772ef806e9c81cce2fb7a7b0dd263527f0896badf74e5ea09ab5560ca804162cde01441054ac5b5d0fca9c39dfc2607cf18086ec6bb4667af2
7
- data.tar.gz: b7ff4072c37645e9dc6f1bd5551bb47ec829cbefd412221bcdf13e5a2e788450243c1e7853e918248a2b1faaea61aec40122ddea32f1ef80a1ae013367d9d349
6
+ metadata.gz: 18a435b9714861bf5f76e516d54b5ed0a3ad4b9d1e192825f2766fab494f90d0579a89d5f5d812b67cc33bb876a174e9341980997ead6a6a5e134bce26637fed
7
+ data.tar.gz: f584db4b3d36d73d0a5fe83439ef75b409e2fe971337a3ad8b25bb9335727053fed1f272057062a2061b0b2b8c01d866f91e894570c77e4320277ea69ead268c
@@ -3,7 +3,7 @@ module Securial
3
3
  skip_authentication!
4
4
 
5
5
  def show
6
- Securial::ENGINE_LOGGER.info("Status check initiated")
6
+ Securial.logger.info("Status check initiated")
7
7
  end
8
8
  end
9
9
  end
@@ -1,6 +1,6 @@
1
1
  module Securial
2
2
  class Role < ApplicationRecord
3
- normalizes :role_name, with: ->(e) { Securial::NormalizingHelper.normalize_role_name(e) }
3
+ normalizes :role_name, with: ->(e) { Securial::Helpers::NormalizingHelper.normalize_role_name(e) }
4
4
 
5
5
  validates :role_name, presence: true, uniqueness: { case_sensitive: false }
6
6
 
@@ -2,7 +2,7 @@ module Securial
2
2
  class User < ApplicationRecord
3
3
  include Securial::PasswordResettable
4
4
 
5
- normalizes :email_address, with: ->(e) { Securial::NormalizingHelper.normalize_email_address(e) }
5
+ normalizes :email_address, with: ->(e) { Securial::Helpers::NormalizingHelper.normalize_email_address(e) }
6
6
 
7
7
  validates :email_address,
8
8
  presence: true,
@@ -12,7 +12,7 @@ module Securial
12
12
  maximum: 255,
13
13
  },
14
14
  format: {
15
- with: Securial::RegexHelper::EMAIL_REGEX,
15
+ with: Securial::Helpers::RegexHelper::EMAIL_REGEX,
16
16
  message: "must be a valid email address",
17
17
  }
18
18
 
@@ -21,7 +21,7 @@ module Securial
21
21
  uniqueness: { case_sensitive: false },
22
22
  length: { maximum: 20 },
23
23
  format: {
24
- with: Securial::RegexHelper::USERNAME_REGEX,
24
+ with: Securial::Helpers::RegexHelper::USERNAME_REGEX,
25
25
  message: "can only contain letters, numbers, underscores, and periods, but cannot start with a number or contain consecutive underscores or periods",
26
26
  }
27
27
 
@@ -11,7 +11,7 @@ Securial.configure do |config|
11
11
  config.log_to_stdout = true # Enable or disable logging to STDOUT
12
12
 
13
13
  # Set log level for file logger: :debug, :info, :warn, :error, :fatal, or :unknown
14
- config.log_file_level = :info
14
+ config.log_file_level = :debug
15
15
 
16
16
  # Set log level for stdout logger
17
17
  config.log_stdout_level = :debug
@@ -1,56 +1,53 @@
1
1
  module Securial
2
2
  module Auth
3
3
  module AuthEncoder
4
- class << self
5
- def encode(session)
6
- return nil unless session && session.class == Securial::Session
7
-
8
- base_payload = {
9
- jti: session.id,
10
- exp: expiry_duration.from_now.to_i,
11
- sub: "session-access-token",
12
- refresh_count: session.refresh_count,
13
- }
14
-
15
- session_payload = {
16
- ip: session.ip_address,
17
- agent: session.user_agent,
18
- }
19
-
20
- payload = base_payload.merge(session_payload)
21
- begin
22
- JWT.encode(payload, secret, algorithm, { kid: "hmac" })
23
- rescue JWT::EncodeError => e
24
- raise Errors::AuthEncodeError, "Failed to encode session: #{e.message}"
25
- end
4
+ module_function
5
+
6
+ def encode(session)
7
+ return nil unless session && session.class == Securial::Session
8
+
9
+ base_payload = {
10
+ jti: session.id,
11
+ exp: expiry_duration.from_now.to_i,
12
+ sub: "session-access-token",
13
+ refresh_count: session.refresh_count,
14
+ }
15
+
16
+ session_payload = {
17
+ ip: session.ip_address,
18
+ agent: session.user_agent,
19
+ }
20
+
21
+ payload = base_payload.merge(session_payload)
22
+ begin
23
+ JWT.encode(payload, secret, algorithm, { kid: "hmac" })
24
+ rescue JWT::EncodeError => e
25
+ raise Errors::AuthEncodeError, "Failed to encode session: #{e.message}"
26
26
  end
27
+ end
27
28
 
28
- def decode(token)
29
- begin
29
+ def decode(token)
30
+ begin
30
31
  decoded = JWT.decode(token, secret, true, { algorithm: algorithm, verify_jti: true, iss: "securial" })
31
- rescue JWT::DecodeError => e
32
- raise Securial::Auth::Errors::AuthDecodeError, "Failed to decode session token: #{e.message}"
33
- end
34
- decoded.first
32
+ rescue JWT::DecodeError => e
33
+ raise Securial::Auth::Errors::AuthDecodeError, "Failed to decode session token: #{e.message}"
35
34
  end
35
+ decoded.first
36
+ end
36
37
 
37
- private
38
-
39
- def secret
40
- # Config::Validation.validate_session_secret!(Securial.configuration)
41
- Securial.configuration.session_secret
42
- end
38
+ def secret
39
+ Securial.configuration.session_secret
40
+ end
43
41
 
44
- def algorithm
45
- # Config::Validation.validate_session_algorithm!(Securial.configuration)
46
- Securial.configuration.session_algorithm.to_s.upcase
47
- end
42
+ def algorithm
43
+ Securial.configuration.session_algorithm.to_s.upcase
44
+ end
48
45
 
49
- def expiry_duration
50
- # Config::Validation.validate_session_expiry_duration!(Securial.configuration)
51
- Securial.configuration.session_expiration_duration
52
- end
46
+ def expiry_duration
47
+ Securial.configuration.session_expiration_duration
53
48
  end
49
+
50
+ private_class_method :secret, :algorithm, :expiry_duration
54
51
  end
55
52
  end
56
53
  end
@@ -1,19 +1,19 @@
1
1
  module Securial
2
2
  module Auth
3
3
  module SessionCreator
4
- class << self
5
- def create_session(user, request)
6
- return nil unless user && user.persisted? && request.is_a?(ActionDispatch::Request)
4
+ module_function
7
5
 
8
- user.sessions.create!(
9
- user_agent: request.user_agent,
10
- ip_address: request.remote_ip,
11
- refresh_token: SecureRandom.hex(64),
12
- last_refreshed_at: Time.current,
13
- refresh_token_expires_at: 1.week.from_now,
14
- ).tap do |session|
15
- Current.session = session
16
- end
6
+ def create_session(user, request)
7
+ return nil unless user && user.persisted? && request.is_a?(ActionDispatch::Request)
8
+
9
+ user.sessions.create!(
10
+ user_agent: request.user_agent,
11
+ ip_address: request.remote_ip,
12
+ refresh_token: SecureRandom.hex(64),
13
+ last_refreshed_at: Time.current,
14
+ refresh_token_expires_at: 1.week.from_now,
15
+ ).tap do |session|
16
+ Current.session = session
17
17
  end
18
18
  end
19
19
  end
@@ -0,0 +1,10 @@
1
+ require "securial/auth/errors"
2
+ require_relative "auth/auth_encoder"
3
+ require_relative "auth/session_creator"
4
+
5
+ module Securial
6
+ module Auth
7
+ # This module serves as a namespace for authentication-related components.
8
+ # It requires all key auth files: Errors, AuthEncoder, SessionCreator.
9
+ end
10
+ end
@@ -1,4 +1,4 @@
1
- require_relative "../helpers/_index"
1
+ require "securial/helpers"
2
2
  module Securial
3
3
  module Config
4
4
  VALID_SESSION_ENCRYPTION_ALGORITHMS = [:hs256, :hs384, :hs512].freeze
@@ -11,7 +11,7 @@ module Securial
11
11
  {
12
12
  log_to_file: !Rails.env.test?,
13
13
  log_to_stdout: !Rails.env.test?,
14
- log_file_level: :info,
14
+ log_file_level: :debug,
15
15
  log_stdout_level: :debug,
16
16
  admin_role: :admin,
17
17
  session_expiration_duration: 3.minutes,
@@ -21,7 +21,7 @@ module Securial
21
21
  password_reset_email_subject: "SECURIAL: Password Reset Instructions",
22
22
  password_min_length: 8,
23
23
  password_max_length: 128,
24
- password_complexity: Securial::RegexHelper::PASSWORD_REGEX,
24
+ password_complexity: Securial::Helpers::RegexHelper::PASSWORD_REGEX,
25
25
  password_expires: true,
26
26
  password_expires_in: 90.days,
27
27
  reset_password_token_expires_in: 2.hours,
@@ -18,19 +18,19 @@ module Securial
18
18
  def validate_admin_role!(config)
19
19
  if config.admin_role.nil? || config.admin_role.to_s.strip.empty?
20
20
  error_message = "Admin role is not set."
21
- Securial::ENGINE_LOGGER.fatal(error_message)
21
+ Securial.logger.fatal(error_message)
22
22
  raise Securial::Config::Errors::ConfigAdminRoleError, error_message
23
23
  end
24
24
 
25
25
  unless config.admin_role.is_a?(Symbol) || config.admin_role.is_a?(String)
26
26
  error_message = "Admin role must be a Symbol or String."
27
- Securial::ENGINE_LOGGER.fatal(error_message)
27
+ Securial.logger.fatal(error_message)
28
28
  raise Securial::Config::Errors::ConfigAdminRoleError, error_message
29
29
  end
30
30
 
31
31
  if config.admin_role.to_s.pluralize.downcase == "accounts"
32
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)
33
+ Securial.logger.fatal(error_message)
34
34
  raise Securial::Config::Errors::ConfigAdminRoleError, error_message
35
35
  end
36
36
  end
@@ -44,16 +44,16 @@ module Securial
44
44
  def validate_session_expiry_duration!(config)
45
45
  if config.session_expiration_duration.nil?
46
46
  error_message = "Session expiration duration is not set."
47
- Securial::ENGINE_LOGGER.fatal(error_message)
47
+ Securial.logger.fatal(error_message)
48
48
  raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
49
49
  end
50
50
  if config.session_expiration_duration.class != ActiveSupport::Duration
51
51
  error_message = "Session expiration duration must be an ActiveSupport::Duration."
52
- Securial::ENGINE_LOGGER.fatal(error_message)
52
+ Securial.logger.fatal(error_message)
53
53
  raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
54
54
  end
55
55
  if config.session_expiration_duration <= 0
56
- Securial::ENGINE_LOGGER.fatal("Session expiration duration must be greater than 0.")
56
+ Securial.logger.fatal("Session expiration duration must be greater than 0.")
57
57
  raise Securial::Config::Errors::ConfigSessionExpirationDurationError, "Session expiration duration must be greater than 0."
58
58
  end
59
59
  end
@@ -61,18 +61,18 @@ module Securial
61
61
  def validate_session_algorithm!(config)
62
62
  if config.session_algorithm.blank?
63
63
  error_message = "Session algorithm is not set."
64
- Securial::ENGINE_LOGGER.fatal(error_message)
64
+ Securial.logger.fatal(error_message)
65
65
  raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
66
66
  end
67
67
  unless config.session_algorithm.is_a?(Symbol)
68
68
  error_message = "Session algorithm must be a Symbol."
69
- Securial::ENGINE_LOGGER.fatal(error_message)
69
+ Securial.logger.fatal(error_message)
70
70
  raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
71
71
  end
72
72
  valid_algorithms = Securial::Config::VALID_SESSION_ENCRYPTION_ALGORITHMS
73
73
  unless valid_algorithms.include?(config.session_algorithm)
74
74
  error_message = "Invalid session algorithm. Valid options are: #{valid_algorithms.map(&:inspect).join(', ')}."
75
- Securial::ENGINE_LOGGER.fatal(error_message)
75
+ Securial.logger.fatal(error_message)
76
76
  raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
77
77
  end
78
78
  end
@@ -80,12 +80,12 @@ module Securial
80
80
  def validate_session_secret!(config)
81
81
  if config.session_secret.blank?
82
82
  error_message = "Session secret is not set."
83
- Securial::ENGINE_LOGGER.fatal(error_message)
83
+ Securial.logger.fatal(error_message)
84
84
  raise Securial::Config::Errors::ConfigSessionSecretError, error_message
85
85
  end
86
86
  unless config.session_secret.is_a?(String)
87
87
  error_message = "Session secret must be a String."
88
- Securial::ENGINE_LOGGER.fatal(error_message)
88
+ Securial.logger.fatal(error_message)
89
89
  raise Securial::Config::Errors::ConfigSessionSecretError, error_message
90
90
  end
91
91
  end
@@ -93,12 +93,12 @@ module Securial
93
93
  def validate_mailer_sender!(config)
94
94
  if config.mailer_sender.blank?
95
95
  error_message = "Mailer sender is not set."
96
- Securial::ENGINE_LOGGER.fatal(error_message)
96
+ Securial.logger.fatal(error_message)
97
97
  raise Securial::Config::Errors::ConfigMailerSenderError, error_message
98
98
  end
99
99
  if config.mailer_sender !~ URI::MailTo::EMAIL_REGEXP
100
100
  error_message = "Mailer sender is not a valid email address."
101
- Securial::ENGINE_LOGGER.fatal(error_message)
101
+ Securial.logger.fatal(error_message)
102
102
  raise Securial::Config::Errors::ConfigMailerSenderError, error_message
103
103
  end
104
104
  end
@@ -114,12 +114,12 @@ module Securial
114
114
  def validate_password_reset_token!(config)
115
115
  if config.reset_password_token_secret.blank?
116
116
  error_message = "Reset password token secret is not set."
117
- Securial::ENGINE_LOGGER.fatal(error_message)
117
+ Securial.logger.fatal(error_message)
118
118
  raise Securial::Config::Errors::ConfigPasswordError, error_message
119
119
  end
120
120
  unless config.reset_password_token_secret.is_a?(String)
121
121
  error_message = "Reset password token secret must be a String."
122
- Securial::ENGINE_LOGGER.fatal(error_message)
122
+ Securial.logger.fatal(error_message)
123
123
  raise Securial::Config::Errors::ConfigPasswordError, error_message
124
124
  end
125
125
  end
@@ -127,12 +127,12 @@ module Securial
127
127
  def validate_password_reset_subject!(config)
128
128
  if config.password_reset_email_subject.blank?
129
129
  error_message = "Password reset email subject is not set."
130
- Securial::ENGINE_LOGGER.fatal(error_message)
130
+ Securial.logger.fatal(error_message)
131
131
  raise Securial::Config::Errors::ConfigPasswordError, error_message
132
132
  end
133
133
  unless config.password_reset_email_subject.is_a?(String)
134
134
  error_message = "Password reset email subject must be a String."
135
- Securial::ENGINE_LOGGER.fatal(error_message)
135
+ Securial.logger.fatal(error_message)
136
136
  raise Securial::Config::Errors::ConfigPasswordError, error_message
137
137
  end
138
138
  end
@@ -140,12 +140,12 @@ module Securial
140
140
  def validate_password_min_max_length!(config)
141
141
  unless config.password_min_length.is_a?(Integer) && config.password_min_length > 0
142
142
  error_message = "Password minimum length must be a positive integer."
143
- Securial::ENGINE_LOGGER.fatal(error_message)
143
+ Securial.logger.fatal(error_message)
144
144
  raise Securial::Config::Errors::ConfigPasswordError, error_message
145
145
  end
146
146
  unless config.password_max_length.is_a?(Integer) && config.password_max_length >= config.password_min_length
147
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)
148
+ Securial.logger.fatal(error_message)
149
149
  raise Securial::Config::Errors::ConfigPasswordError, error_message
150
150
  end
151
151
  end
@@ -153,7 +153,7 @@ module Securial
153
153
  def validate_password_complexity!(config)
154
154
  if config.password_complexity.nil? || !config.password_complexity.is_a?(Regexp)
155
155
  error_message = "Password complexity regex is not set or is not a valid Regexp."
156
- Securial::ENGINE_LOGGER.fatal(error_message)
156
+ Securial.logger.fatal(error_message)
157
157
  raise Securial::Config::Errors::ConfigPasswordError, error_message
158
158
  end
159
159
  end
@@ -161,7 +161,7 @@ module Securial
161
161
  def validate_password_expiration!(config)
162
162
  unless config.password_expires.is_a?(TrueClass) || config.password_expires.is_a?(FalseClass)
163
163
  error_message = "Password expiration must be a boolean value."
164
- Securial::ENGINE_LOGGER.fatal(error_message)
164
+ Securial.logger.fatal(error_message)
165
165
  raise Securial::Config::Errors::ConfigPasswordError, error_message
166
166
  end
167
167
 
@@ -171,7 +171,7 @@ module Securial
171
171
  config.password_expires_in <= 0
172
172
  )
173
173
  error_message = "Password expiration duration is not set or is not a valid ActiveSupport::Duration."
174
- Securial::ENGINE_LOGGER.fatal(error_message)
174
+ Securial.logger.fatal(error_message)
175
175
  raise Securial::Config::Errors::ConfigPasswordError, error_message
176
176
  end
177
177
  end
@@ -185,7 +185,7 @@ module Securial
185
185
  valid_formats = Securial::Config::VALID_RESPONSE_KEYS_FORMATS
186
186
  unless valid_formats.include?(config.response_keys_format)
187
187
  error_message = "Invalid response_keys_format option. Valid options are: #{valid_formats.map(&:inspect).join(', ')}."
188
- Securial::ENGINE_LOGGER.fatal(error_message)
188
+ Securial.logger.fatal(error_message)
189
189
  raise Securial::Config::Errors::ConfigResponseError, error_message
190
190
  end
191
191
  end
@@ -194,7 +194,7 @@ module Securial
194
194
  valid_options = Securial::Config::VALID_TIMESTAMP_OPTIONS
195
195
  unless valid_options.include?(config.timestamps_in_response)
196
196
  error_message = "Invalid timestamps_in_response option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
197
- Securial::ENGINE_LOGGER.fatal(error_message)
197
+ Securial.logger.fatal(error_message)
198
198
  raise Securial::Config::Errors::ConfigResponseError, error_message
199
199
  end
200
200
  end
@@ -208,7 +208,7 @@ module Securial
208
208
  valid_options = Securial::Config::VALID_SECURITY_HEADERS
209
209
  unless valid_options.include?(config.security_headers)
210
210
  error_message = "Invalid security_headers option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
211
- Securial::ENGINE_LOGGER.fatal(error_message)
211
+ Securial.logger.fatal(error_message)
212
212
  raise Securial::Config::Errors::ConfigSecurityError, error_message
213
213
  end
214
214
  end
@@ -216,7 +216,7 @@ module Securial
216
216
  def validate_rate_limiting!(config) # rubocop:disable Metrics/MethodLength
217
217
  unless config.rate_limiting_enabled.is_a?(TrueClass) || config.rate_limiting_enabled.is_a?(FalseClass)
218
218
  error_message = "rate_limiting_enabled must be a boolean value."
219
- Securial::ENGINE_LOGGER.fatal(error_message)
219
+ Securial.logger.fatal(error_message)
220
220
  raise Securial::Config::Errors::ConfigSecurityError, error_message
221
221
  end
222
222
 
@@ -227,19 +227,19 @@ module Securial
227
227
  config.rate_limit_requests_per_minute > 0
228
228
 
229
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)
230
+ Securial.logger.fatal(error_message)
231
231
  raise Securial::Config::Errors::ConfigSecurityError, error_message
232
232
  end
233
233
 
234
234
  unless config.rate_limit_response_status.is_a?(Integer) && config.rate_limit_response_status.between?(400, 599)
235
235
  error_message = "rate_limit_response_status must be an HTTP status code between 4xx and 5xx."
236
- Securial::ENGINE_LOGGER.fatal(error_message)
236
+ Securial.logger.fatal(error_message)
237
237
  raise Securial::Config::Errors::ConfigSecurityError, error_message
238
238
  end
239
239
 
240
240
  unless config.rate_limit_response_message.is_a?(String) && !config.rate_limit_response_message.strip.empty?
241
241
  error_message = "rate_limit_response_message must be a non-empty String."
242
- Securial::ENGINE_LOGGER.fatal(error_message)
242
+ Securial.logger.fatal(error_message)
243
243
  raise Securial::Config::Errors::ConfigSecurityError, error_message
244
244
  end
245
245
  end
@@ -0,0 +1,10 @@
1
+ require "securial/config/configuration"
2
+ require "securial/config/validation"
3
+ require "securial/config/errors"
4
+
5
+ module Securial
6
+ module Config
7
+ # This module acts as a namespace for configuration-related components.
8
+ # It requires all key config files: Configuration, Validation, Errors.
9
+ end
10
+ end
@@ -1,13 +1,15 @@
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"
7
-
8
- require_relative "./middleware/_index"
9
1
  require "jwt"
10
2
 
3
+ require "securial/logger"
4
+ require "securial/key_transformer"
5
+ require "securial/config"
6
+ require "securial/helpers"
7
+ require "securial/auth"
8
+ require "securial/inspectors"
9
+ require "securial/middlewares"
10
+ require "securial/security"
11
+ require "securial/version_checker"
12
+
11
13
  module Securial
12
14
  class Engine < ::Rails::Engine
13
15
  isolate_namespace Securial
@@ -53,13 +55,22 @@ module Securial
53
55
  end
54
56
 
55
57
  initializer "securial.middleware" do |app|
56
- middleware.use Securial::Middleware::RequestLoggerTag
57
- middleware.use Securial::Middleware::TransformRequestKeys
58
- middleware.use Securial::Middleware::TransformResponseKeys
58
+ middleware.use Securial::Middlewares::RequestLoggerTag
59
+ middleware.use Securial::Middlewares::TransformRequestKeys
60
+ middleware.use Securial::Middlewares::TransformResponseKeys
61
+ end
62
+
63
+ initializer "securial.security.request_rate_limiter" do |app|
59
64
  if Securial.configuration.rate_limiting_enabled
60
- require "rack/attack"
61
- require_relative "./rack_attack"
62
- middleware.use Rack::Attack
65
+ Securial::Security::RequestRateLimiter.apply!
66
+ Rails.application.config.middleware.use Rack::Attack
67
+ end
68
+ end
69
+
70
+
71
+ initializer "securial.version_check" do
72
+ config.after_initialize do
73
+ Securial::VersionChecker.check_latest_version
63
74
  end
64
75
  end
65
76
 
@@ -86,7 +97,7 @@ module Securial
86
97
  private
87
98
 
88
99
  def log(message)
89
- Securial::ENGINE_LOGGER.info("[Securial] #{message}")
100
+ Securial.logger.info("[Securial] #{message}")
90
101
  end
91
102
  end
92
103
  end
@@ -1,17 +1,19 @@
1
1
  module Securial
2
- module NormalizingHelper
3
- module_function
2
+ module Helpers
3
+ module NormalizingHelper
4
+ module_function
4
5
 
5
- def normalize_email_address(email)
6
- return "" if email.empty?
6
+ def normalize_email_address(email)
7
+ return "" if email.empty?
7
8
 
8
- email.strip.downcase
9
- end
9
+ email.strip.downcase
10
+ end
10
11
 
11
- def normalize_role_name(role_name)
12
- return "" if role_name.empty?
12
+ def normalize_role_name(role_name)
13
+ return "" if role_name.empty?
13
14
 
14
- role_name.strip.downcase.titleize
15
+ role_name.strip.downcase.titleize
16
+ end
15
17
  end
16
18
  end
17
19
  end
@@ -1,16 +1,18 @@
1
1
  module Securial
2
- module RegexHelper
3
- EMAIL_REGEX = URI::MailTo::EMAIL_REGEXP
4
- USERNAME_REGEX = /\A(?![0-9])[a-zA-Z](?:[a-zA-Z0-9]|[._](?![._]))*[a-zA-Z0-9]\z/
5
- PASSWORD_REGEX = %r{\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])[a-zA-Z].*\z}
2
+ module Helpers
3
+ module RegexHelper
4
+ EMAIL_REGEX = URI::MailTo::EMAIL_REGEXP
5
+ USERNAME_REGEX = /\A(?![0-9])[a-zA-Z](?:[a-zA-Z0-9]|[._](?![._]))*[a-zA-Z0-9]\z/
6
+ PASSWORD_REGEX = %r{\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])[a-zA-Z].*\z}
6
7
 
7
- class << self
8
- def valid_email?(email)
9
- email.match?(EMAIL_REGEX)
10
- end
8
+ class << self
9
+ def valid_email?(email)
10
+ email.match?(EMAIL_REGEX)
11
+ end
11
12
 
12
- def valid_username?(username)
13
- username.match?(USERNAME_REGEX)
13
+ def valid_username?(username)
14
+ username.match?(USERNAME_REGEX)
15
+ end
14
16
  end
15
17
  end
16
18
  end
@@ -0,0 +1,9 @@
1
+ require "securial/helpers/normalizing_helper"
2
+ require "securial/helpers/regex_helper"
3
+
4
+ module Securial
5
+ module Helpers
6
+ # This module acts as a namespace for helper modules.
7
+ # It requires all helper modules to make them available for consumers.
8
+ end
9
+ end
@@ -17,27 +17,27 @@ module Securial
17
17
 
18
18
  # rubocop:disable Rails/Output
19
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
20
+ Securial.logger.debug "Securial engine routes:"
21
+ Securial.logger.debug "Total routes: #{filtered.size}"
22
+ Securial.logger.debug "Filtered by controller: #{controller}" if controller
23
+ Securial.logger.debug "Filtered routes: #{filtered.size}" if controller
24
+ Securial.logger.debug "-" * 120
25
+ Securial.logger.debug "#{'Verb'.ljust(8)} #{'Path'.ljust(45)} #{'Controller#Action'.ljust(40)} Name"
26
+ Securial.logger.debug "-" * 120
27
27
  end
28
28
 
29
29
  def print_details(filtered, controller) # rubocop:disable Rails/Output
30
30
  if filtered.empty?
31
31
  if controller
32
- Securial::ENGINE_LOGGER.debug "No routes found for controller: #{controller}"
32
+ Securial.logger.debug "No routes found for controller: #{controller}"
33
33
  else
34
- Securial::ENGINE_LOGGER.debug "No routes found for Securial engine"
34
+ Securial.logger.debug "No routes found for Securial engine"
35
35
  end
36
- Securial::ENGINE_LOGGER.debug "-" * 120
36
+ Securial.logger.debug "-" * 120
37
37
  return
38
38
  end
39
39
 
40
- Securial::ENGINE_LOGGER.debug filtered.map { |r|
40
+ Securial.logger.debug filtered.map { |r|
41
41
  name = r.name || ""
42
42
  verb = r.verb.to_s.ljust(8)
43
43
  path = r.path.spec.to_s.sub(/\(\.:format\)/, "").ljust(45)
@@ -0,0 +1,8 @@
1
+ require "securial/inspectors/route_inspector"
2
+
3
+ module Securial
4
+ module Inspectors
5
+ # This module serves as a namespace for inspectors.
6
+ # It requires the RouteInspector to provide route inspection capabilities.
7
+ end
8
+ end
@@ -0,0 +1,48 @@
1
+ module Securial
2
+ module Logger
3
+ class Broadcaster
4
+ def initialize(*targets)
5
+ @targets = targets
6
+ end
7
+
8
+ ::Logger::Severity.constants.each do |severity|
9
+ method = severity.downcase
10
+ define_method(method) do |*args, &block|
11
+ @targets.each do |logger|
12
+ if logger.level <= ::Logger::Severity.const_get(severity)
13
+ logger.send(method, *args, &block)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def add(severity, message = nil, progname = nil, &block)
20
+ @targets.each do |logger|
21
+ if logger.level <= severity.to_i
22
+ logger.add(severity, message, progname, &block)
23
+ end
24
+ end
25
+ end
26
+
27
+ def <<(msg)
28
+ @targets.each { |logger| logger << msg if logger.respond_to?(:<<) }
29
+ end
30
+
31
+ def close
32
+ @targets.each { |logger| logger.close if logger.respond_to?(:close) }
33
+ end
34
+
35
+ def flush
36
+ @targets.each { |logger| logger.flush if logger.respond_to?(:flush) }
37
+ end
38
+
39
+ def formatter
40
+ @targets.first.formatter if @targets.any?
41
+ end
42
+
43
+ def formatter=(formatter)
44
+ @targets.each { |logger| logger.formatter = formatter }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,60 @@
1
+ require "logger"
2
+ require "active_support/logger"
3
+ require "active_support/tagged_logging"
4
+ require_relative "colors"
5
+ require_relative "broadcaster"
6
+
7
+ module Securial
8
+ module Logger
9
+ def self.build # rubocop:disable Metrics/MethodLength
10
+ loggers = []
11
+ colorize_stdout = false
12
+
13
+ unless Securial.configuration.log_to_file == false
14
+ file_logger = ::Logger.new(Rails.root.join("log", "securial.log"))
15
+ file_logger.level = resolve_level(Securial.configuration.log_file_level)
16
+ file_logger.formatter = ::Logger::Formatter.new
17
+ loggers << file_logger
18
+ end
19
+
20
+ unless Securial.configuration.log_to_stdout == false
21
+ stdout_logger = ::Logger.new(STDOUT)
22
+ stdout_logger.level = resolve_level(Securial.configuration.log_stdout_level)
23
+ stdout_logger.formatter = proc do |severity, timestamp, progname, msg|
24
+ color = COLORS[severity] || CLEAR
25
+ padded = severity.ljust(SEVERITY_WIDTH)
26
+ formatted = "#{timestamp.strftime("%Y-%m-%d %H:%M:%S")} #{padded} -- : #{msg}\n"
27
+ "#{color}#{formatted}#{CLEAR}"
28
+ end
29
+ colorize_stdout = true
30
+ loggers << stdout_logger
31
+ end
32
+
33
+ if loggers.empty?
34
+ null_logger = ::Logger.new(IO::NULL)
35
+ return ActiveSupport::TaggedLogging.new(null_logger)
36
+ end
37
+
38
+ broadcaster = Broadcaster.new(*loggers)
39
+ tagged_logger = ActiveSupport::TaggedLogging.new(broadcaster)
40
+
41
+ if colorize_stdout && !Rails.env.test?
42
+ tagged_logger.formatter = proc do |severity, timestamp, progname, msg|
43
+ color = COLORS[severity] || CLEAR
44
+ padded = severity.ljust(SEVERITY_WIDTH)
45
+ formatted = "#{timestamp.strftime("%Y-%m-%d %H:%M:%S")} #{padded} -- : #{msg}\n"
46
+ "#{color}#{formatted}#{CLEAR}"
47
+ end
48
+ end
49
+
50
+ tagged_logger
51
+ end
52
+
53
+ def self.resolve_level(level)
54
+ return ::Logger::INFO if level.nil?
55
+ ::Logger.const_get(level.to_s.upcase)
56
+ rescue NameError
57
+ ::Logger::INFO
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,14 @@
1
+ module Securial
2
+ module Logger
3
+ COLORS = {
4
+ "DEBUG" => "\e[36m", # cyan
5
+ "INFO" => "\e[32m", # green
6
+ "WARN" => "\e[33m", # yellow
7
+ "ERROR" => "\e[31m", # red
8
+ "FATAL" => "\e[35m", # magenta
9
+ "UNKNOWN" => "\e[37m", # white
10
+ }.freeze
11
+ CLEAR = "\e[0m"
12
+ SEVERITY_WIDTH = 5
13
+ end
14
+ end
@@ -1,71 +1,8 @@
1
- require "logger"
2
- require "active_support/logger"
3
- require "active_support/tagged_logging"
1
+ require_relative "logger/builder"
4
2
 
5
3
  module Securial
6
- class Logger
7
- def self.build
8
- outputs = []
9
-
10
- unless Securial.configuration.log_to_file == false
11
- log_file = Rails.root.join("log", "securial.log").open("a")
12
- log_file.sync = true
13
- outputs << log_file
14
- end
15
-
16
- unless Securial.configuration.log_to_stdout == false
17
- outputs << STDOUT
18
- end
19
-
20
- if outputs.empty?
21
- null_logger = ::Logger.new(IO::NULL)
22
- return ActiveSupport::TaggedLogging.new(null_logger)
23
- end
24
-
25
- logger = ActiveSupport::Logger.new(MultiIO.new(*outputs))
26
- logger.level = resolve_log_level
27
- logger.formatter = ::Logger::Formatter.new
28
-
29
- ActiveSupport::TaggedLogging.new(logger)
30
- end
31
-
32
- def self.resolve_log_level
33
- file_level = Securial.configuration.log_file_level
34
- stdout_level = Securial.configuration.log_stdout_level
35
-
36
- # Use the lower (more verbose) level of the two
37
- levels = [file_level, stdout_level].compact.map do |lvl|
38
- begin
39
- ::Logger.const_get(lvl.to_s.upcase)
40
- rescue NameError
41
- nil
42
- end
43
- end.compact
44
-
45
- levels.min || ::Logger::INFO
46
- end
47
-
48
- private
49
-
50
- class MultiIO
51
- def initialize(*targets)
52
- @targets = targets
53
- end
54
-
55
- def write(*args)
56
- @targets.each { |t| t.write(*args) }
57
- end
58
-
59
- def close
60
- @targets.each do |t|
61
- next if [STDOUT, STDERR].include?(t)
62
- t.close
63
- end
64
- end
65
-
66
- def flush
67
- @targets.each { |t| t.flush if t.respond_to?(:flush) }
68
- end
69
- end
4
+ module Logger
5
+ # This module serves as a namespace for logging functionality.
6
+ # It requires the Logger::Builder to provide logger building capabilities.
70
7
  end
71
8
  end
@@ -0,0 +1,19 @@
1
+ module Securial
2
+ module Middlewares
3
+ class RequestLoggerTag
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ request_id = env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
10
+ tags = ["Securial"]
11
+ tags << request_id if request_id
12
+
13
+ logger = Securial.logger || Rails.logger || ::Logger.new(IO::NULL)
14
+ tagged_logger = logger.is_a?(ActiveSupport::TaggedLogging) ? logger : ActiveSupport::TaggedLogging.new(logger)
15
+ tagged_logger.tagged(*tags) { @app.call(env) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,8 +1,8 @@
1
- # lib/securial/middleware/transform_request_keys.rb
1
+ # lib/securial/middlewares/transform_request_keys.rb
2
2
  require "json"
3
3
 
4
4
  module Securial
5
- module Middleware
5
+ module Middlewares
6
6
  class TransformRequestKeys
7
7
  def initialize(app)
8
8
  @app = app
@@ -1,8 +1,7 @@
1
- # lib/securial/middleware/transform_response_keys.rb
2
1
  require "json"
3
2
 
4
3
  module Securial
5
- module Middleware
4
+ module Middlewares
6
5
  class TransformResponseKeys
7
6
  def initialize(app)
8
7
  @app = app
@@ -16,13 +15,21 @@ module Securial
16
15
 
17
16
  if body.present?
18
17
  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
18
 
23
- new_body = [JSON.generate(transformed)]
24
- headers["Content-Length"] = new_body.first.bytesize.to_s
25
- return [status, headers, new_body]
19
+ # Only transform if not snake_case
20
+ if format != :snake_case
21
+ begin
22
+ transformed = Securial::KeyTransformer.deep_transform_keys(JSON.parse(body)) do |key|
23
+ Securial::KeyTransformer.camelize(key, format)
24
+ end
25
+
26
+ new_body = [JSON.generate(transformed)]
27
+ headers["Content-Length"] = new_body.first.bytesize.to_s
28
+ return [status, headers, new_body]
29
+ rescue JSON::ParserError
30
+ # If not valid JSON, fall through and return original response
31
+ end
32
+ end
26
33
  end
27
34
  end
28
35
 
@@ -0,0 +1,10 @@
1
+ require_relative "middlewares/transform_request_keys"
2
+ require_relative "middlewares/transform_response_keys"
3
+ require_relative "middlewares/request_logger_tag"
4
+
5
+ module Securial
6
+ module Middleware
7
+ # This module serves as a namespace for middlewares.
8
+ # It requires the necessary middleware files to provide functionality.
9
+ end
10
+ end
@@ -0,0 +1,68 @@
1
+ require "rack/attack"
2
+ require "securial/config"
3
+ require "securial/logger"
4
+
5
+ module Securial
6
+ module Security
7
+ module RequestRateLimiter
8
+ module_function
9
+
10
+ def apply! # rubocop:disable Metrics/MethodLength
11
+ resp_status = Securial.configuration.rate_limit_response_status
12
+ resp_message = Securial.configuration.rate_limit_response_message
13
+ # Throttle login attempts by IP
14
+ Rack::Attack.throttle("securial/logins/ip",
15
+ limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
16
+ period: 1.minute
17
+ ) do |req|
18
+ if req.path.include?("sessions/login") && req.post?
19
+ req.ip
20
+ end
21
+ end
22
+
23
+ # Throttle login attempts by username/email
24
+ Rack::Attack.throttle("securial/logins/email",
25
+ limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
26
+ period: 1.minute
27
+ ) do |req|
28
+ if req.path.include?("sessions/login") && req.post?
29
+ req.params["email_address"].to_s.downcase.strip
30
+ end
31
+ end
32
+
33
+ # Throttle password reset requests by IP
34
+ Rack::Attack.throttle("securial/password_resets/ip",
35
+ limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
36
+ period: 1.minute
37
+ ) do |req|
38
+ if req.path.include?("password/forgot") && req.post?
39
+ req.ip
40
+ end
41
+ end
42
+
43
+ # Throttle password reset requests by email
44
+ Rack::Attack.throttle("securial/password_resets/email",
45
+ limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
46
+ period: 1.minute
47
+ ) do |req|
48
+ if req.path.include?("password/forgot") && req.post?
49
+ req.params["email_address"].to_s.downcase.strip
50
+ end
51
+ end
52
+
53
+ # Custom response for throttled requests
54
+ Rack::Attack.throttled_responder = lambda do |request|
55
+ retry_after = (request.env["rack.attack.match_data"] || {})[:period]
56
+ [
57
+ resp_status,
58
+ {
59
+ "Content-Type" => "application/json",
60
+ "Retry-After" => retry_after.to_s,
61
+ },
62
+ [{ error: resp_message }.to_json]
63
+ ]
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ require "securial/security/request_rate_limiter"
2
+
3
+ module Securial
4
+ module Security
5
+ # This module serves as a namespace for security-related functionality.
6
+ # It can be extended with additional security features in the future.
7
+ end
8
+ end
@@ -1,3 +1,3 @@
1
1
  module Securial
2
- VERSION = "0.6.1".freeze
2
+ VERSION = "0.7.0".freeze
3
3
  end
@@ -0,0 +1,31 @@
1
+ require "net/http"
2
+ require "json"
3
+
4
+ module Securial
5
+ module VersionChecker
6
+ module_function
7
+
8
+ def check_latest_version
9
+ begin
10
+ rubygems_api_url = "https://rubygems.org/api/v1/versions/securial/latest.json"
11
+ uri = URI(rubygems_api_url)
12
+ http = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 5, read_timeout: 5)
13
+ response = http.request(Net::HTTP::Get.new(uri))
14
+ latest = JSON.parse(response.body)["version"]
15
+
16
+ current = Securial::VERSION
17
+ if Gem::Version.new(latest) > Gem::Version.new(current)
18
+ Securial.logger.info "A newer version (#{latest}) of Securial is available. You are using #{current}."
19
+ Securial.logger.info "Please consider updating!"
20
+ Securial.logger.debug "You can update Securial by running the following command in your terminal:"
21
+ Securial.logger.debug "`bundle update securial`"
22
+ else
23
+ Securial.logger.info "You are using the latest version of Securial (#{current})."
24
+ Securial.logger.debug "No updates available at this time."
25
+ end
26
+ rescue => e
27
+ Securial.logger.debug("Version check failed: #{e.message}")
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/securial.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  require "securial/version"
2
2
  require "securial/engine"
3
+ require "securial/logger"
3
4
 
4
5
  require "jbuilder"
5
6
 
6
7
  module Securial
7
8
  class << self
8
9
  attr_accessor :configuration
10
+ attr_accessor :logger
9
11
 
10
12
  def configuration
11
13
  @configuration ||= Securial::Config::Configuration.new
@@ -20,6 +22,10 @@ module Securial
20
22
  yield(configuration)
21
23
  end
22
24
 
25
+ def logger
26
+ @logger ||= Securial::Logger.build
27
+ end
28
+
23
29
  # Returns the pluralized form of the admin role.
24
30
  # This behavior is intentional and aligns with the project's routing conventions.
25
31
  def admin_namespace
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: securial
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aly Badawy
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-30 00:00:00.000000000 Z
10
+ date: 2025-06-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -251,7 +251,7 @@ files:
251
251
  - lib/generators/factory_bot/templates/factory.erb
252
252
  - lib/generators/securial/install/install_generator.rb
253
253
  - lib/generators/securial/install/templates/securial_initializer.erb
254
- - lib/generators/securial/install/views_generastor.rb
254
+ - lib/generators/securial/install/views_generator.rb
255
255
  - lib/generators/securial/jbuilder/jbuilder_generator.rb
256
256
  - lib/generators/securial/jbuilder/templates/_resource.json.erb
257
257
  - lib/generators/securial/jbuilder/templates/index.json.erb
@@ -262,11 +262,11 @@ files:
262
262
  - lib/generators/securial/scaffold/templates/routes.erb
263
263
  - lib/generators/securial/scaffold/templates/routing_spec.erb
264
264
  - lib/securial.rb
265
- - lib/securial/auth/_index.rb
265
+ - lib/securial/auth.rb
266
266
  - lib/securial/auth/auth_encoder.rb
267
267
  - lib/securial/auth/errors.rb
268
268
  - lib/securial/auth/session_creator.rb
269
- - lib/securial/config/_index.rb
269
+ - lib/securial/config.rb
270
270
  - lib/securial/config/configuration.rb
271
271
  - lib/securial/config/errors.rb
272
272
  - lib/securial/config/validation.rb
@@ -275,25 +275,30 @@ files:
275
275
  - lib/securial/factories/securial/roles.rb
276
276
  - lib/securial/factories/securial/sessions.rb
277
277
  - lib/securial/factories/securial/users.rb
278
- - lib/securial/helpers/_index.rb
278
+ - lib/securial/helpers.rb
279
279
  - lib/securial/helpers/normalizing_helper.rb
280
280
  - lib/securial/helpers/regex_helper.rb
281
- - lib/securial/inspectors/_index.rb
281
+ - lib/securial/inspectors.rb
282
282
  - lib/securial/inspectors/route_inspector.rb
283
283
  - lib/securial/key_transformer.rb
284
284
  - lib/securial/logger.rb
285
- - lib/securial/middleware/_index.rb
286
- - lib/securial/middleware/request_logger_tag.rb
287
- - lib/securial/middleware/transform_request_keys.rb
288
- - lib/securial/middleware/transform_response_keys.rb
289
- - lib/securial/rack_attack.rb
285
+ - lib/securial/logger/broadcaster.rb
286
+ - lib/securial/logger/builder.rb
287
+ - lib/securial/logger/colors.rb
288
+ - lib/securial/middlewares.rb
289
+ - lib/securial/middlewares/request_logger_tag.rb
290
+ - lib/securial/middlewares/transform_request_keys.rb
291
+ - lib/securial/middlewares/transform_response_keys.rb
292
+ - lib/securial/security.rb
293
+ - lib/securial/security/request_rate_limiter.rb
290
294
  - lib/securial/version.rb
295
+ - lib/securial/version_checker.rb
291
296
  - lib/tasks/securial_tasks.rake
292
297
  homepage: https://github.com/AlyBadawy/Securial/wiki
293
298
  licenses:
294
299
  - MIT
295
300
  metadata:
296
- release_date: '2025-05-30'
301
+ release_date: '2025-06-01'
297
302
  allowed_push_host: https://rubygems.org
298
303
  homepage_uri: https://github.com/AlyBadawy/Securial/wiki
299
304
  source_code_uri: https://github.com/AlyBadawy/Securial
@@ -1,3 +0,0 @@
1
- require_relative "errors"
2
- require_relative "auth_encoder"
3
- require_relative "session_creator"
@@ -1,3 +0,0 @@
1
- require_relative "./configuration"
2
- require_relative "./validation"
3
- require_relative "./errors"
@@ -1,2 +0,0 @@
1
- require_relative "normalizing_helper"
2
- require_relative "regex_helper"
@@ -1 +0,0 @@
1
- require_relative "route_inspector"
@@ -1,3 +0,0 @@
1
- require_relative "./transform_request_keys"
2
- require_relative "./transform_response_keys"
3
- require_relative "./request_logger_tag"
@@ -1,18 +0,0 @@
1
- module Securial
2
- module Middleware
3
- class RequestLoggerTag
4
- def initialize(app)
5
- @app = app
6
- end
7
-
8
- def call(env)
9
- request = ActionDispatch::Request.new(env)
10
- request_id = request.request_id || SecureRandom.uuid
11
-
12
- Securial::ENGINE_LOGGER.tagged("Securial", "RequestID:#{request_id}") do
13
- @app.call(env)
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,48 +0,0 @@
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