securial 0.8.1 → 1.0.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -16
  3. data/app/controllers/concerns/securial/identity.rb +18 -9
  4. data/app/controllers/securial/status_controller.rb +2 -0
  5. data/app/controllers/securial/users_controller.rb +1 -1
  6. data/app/views/securial/status/show.json.jbuilder +1 -1
  7. data/bin/securial +20 -52
  8. data/db/migrate/20250606182648_seed_roles_and_users.rb +69 -0
  9. data/lib/generators/securial/install/templates/securial_initializer.erb +115 -18
  10. data/lib/securial/cli/run.rb +11 -0
  11. data/lib/securial/cli/securial_new.rb +53 -0
  12. data/lib/securial/cli/show_help.rb +26 -0
  13. data/lib/securial/cli/show_version.rb +9 -0
  14. data/lib/securial/config/configuration.rb +3 -53
  15. data/lib/securial/config/signature.rb +107 -0
  16. data/lib/securial/config/validation.rb +58 -14
  17. data/lib/securial/config.rb +2 -0
  18. data/lib/securial/engine.rb +2 -0
  19. data/lib/securial/engine_initializers.rb +21 -2
  20. data/lib/securial/error/config.rb +0 -28
  21. data/lib/securial/helpers/key_transformer.rb +33 -0
  22. data/lib/securial/helpers.rb +1 -0
  23. data/lib/securial/middleware/response_headers.rb +19 -0
  24. data/lib/securial/middleware/transform_request_keys.rb +35 -0
  25. data/lib/securial/middleware/transform_response_keys.rb +47 -0
  26. data/lib/securial/middleware.rb +3 -0
  27. data/lib/securial/security/request_rate_limiter.rb +45 -0
  28. data/lib/securial/security.rb +8 -0
  29. data/lib/securial/version.rb +1 -1
  30. data/lib/tasks/securial_routes.rake +26 -0
  31. metadata +44 -19
  32. data/lib/securial/config/validation/logger_validation.rb +0 -29
  33. data/lib/securial/config/validation/mailer_validation.rb +0 -24
  34. data/lib/securial/config/validation/password_validation.rb +0 -91
  35. data/lib/securial/config/validation/response_validation.rb +0 -37
  36. data/lib/securial/config/validation/roles_validation.rb +0 -32
  37. data/lib/securial/config/validation/security_validation.rb +0 -56
  38. data/lib/securial/config/validation/session_validation.rb +0 -87
@@ -1,67 +1,17 @@
1
1
  require "securial/config/validation"
2
+ require "securial/config/signature"
2
3
  require "securial/helpers/regex_helper"
3
4
 
4
5
  module Securial
5
6
  module Config
6
7
  class Configuration
7
- def self.default_config_attributes # rubocop:disable Metrics/MethodLength
8
- {
9
- # General configuration
10
- app_name: "Securial",
11
-
12
- # Logger configuration
13
- log_to_file: !Rails.env.test?,
14
- log_to_stdout: !Rails.env.test?,
15
- log_file_level: :debug,
16
- log_stdout_level: :debug,
17
-
18
- # Roles configuration
19
- admin_role: :admin,
20
-
21
- session_expiration_duration: 3.minutes,
22
- session_secret: "secret",
23
- session_algorithm: :hs256,
24
- session_refresh_token_expires_in: 1.week,
25
-
26
- # Mailer configuration
27
- mailer_sender: "no-reply@example.com",
28
- mailer_sign_up_enabled: true,
29
- mailer_sign_up_subject: "SECURIAL: Welcome to Our Service",
30
- mailer_sign_in_enabled: true,
31
- mailer_sign_in_subject: "SECURIAL: Sign In Notification",
32
- mailer_update_account_enabled: true,
33
- mailer_update_account_subject: "SECURIAL: Account Update Notification",
34
- mailer_forgot_password_subject: "SECURIAL: Password Reset Instructions",
35
-
36
- # Password configuration
37
- password_min_length: 8,
38
- password_max_length: 128,
39
- password_complexity: Securial::Helpers::RegexHelper::PASSWORD_REGEX,
40
- password_expires: true,
41
- password_expires_in: 90.days,
42
- reset_password_token_expires_in: 2.hours,
43
- reset_password_token_secret: "reset_secret",
44
-
45
- # Response configuration
46
- response_keys_format: :snake_case,
47
- timestamps_in_response: :all,
48
-
49
- # Security configuration
50
- security_headers: :strict,
51
- rate_limiting_enabled: true,
52
- rate_limit_requests_per_minute: 60,
53
- rate_limit_response_status: 429,
54
- rate_limit_response_message: "Too many requests, please try again later.",
55
- }
56
- end
57
-
58
8
  def initialize
59
- self.class.default_config_attributes.each do |attr, default_value|
9
+ Securial::Config::Signature.default_config_attributes.each do |attr, default_value|
60
10
  instance_variable_set("@#{attr}", default_value)
61
11
  end
62
12
  end
63
13
 
64
- default_config_attributes.each_key do |attr|
14
+ Securial::Config::Signature.default_config_attributes.each_key do |attr|
65
15
  define_method(attr) { instance_variable_get("@#{attr}") }
66
16
 
67
17
  define_method("#{attr}=") do |value|
@@ -0,0 +1,107 @@
1
+ module Securial
2
+ module Config
3
+ module Signature
4
+ LOG_LEVELS = %i[debug info warn error fatal unknown].freeze
5
+ SESSION_ALGORITHMS = %i[hs256 hs384 hs512].freeze
6
+ SECURITY_HEADERS = %i[strict default none].freeze
7
+ TIMESTAMP_OPTIONS = %i[all admins_only none].freeze
8
+ RESPONSE_KEYS_FORMATS = %i[snake_case lowerCamelCase UpperCamelCase].freeze
9
+
10
+ extend self
11
+
12
+ def config_signature
13
+ [
14
+ general_signature,
15
+ logger_signature,
16
+ roles_signature,
17
+ session_signature,
18
+ mailer_signature,
19
+ password_signature,
20
+ response_signature,
21
+ security_signature,
22
+ ].reduce({}, :merge)
23
+ end
24
+
25
+ def default_config_attributes
26
+ config_signature.transform_values do |options|
27
+ options[:default]
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def general_signature
34
+ {
35
+ app_name: { type: String, required: true, default: "Securial" },
36
+ }
37
+ end
38
+
39
+ def logger_signature
40
+ {
41
+ log_to_file: { type: [TrueClass, FalseClass], required: true, default: Rails.env.test? ? false : true },
42
+ log_file_level: { type: Symbol, required: "log_to_file", allowed_values: LOG_LEVELS, default: :debug },
43
+ log_to_stdout: { type: [TrueClass, FalseClass], required: true, default: Rails.env.test? ? false : true },
44
+ log_stdout_level: { type: Symbol, required: "log_to_stdout", allowed_values: LOG_LEVELS, default: :debug },
45
+ }
46
+ end
47
+
48
+ def roles_signature
49
+ {
50
+ admin_role: { type: String, required: true, default: "admin" },
51
+ }
52
+ end
53
+
54
+ def session_signature
55
+ {
56
+ session_expiration_duration: { type: ActiveSupport::Duration, required: true, default: 3.minutes },
57
+ session_secret: { type: String, required: true, default: "secret" },
58
+ session_algorithm: { type: Symbol, required: true, allowed_values: SESSION_ALGORITHMS, default: :hs256 },
59
+ session_refresh_token_expires_in: { type: ActiveSupport::Duration, required: true, default: 1.week },
60
+ }
61
+ end
62
+
63
+ def mailer_signature
64
+ {
65
+ mailer_sender: { type: String, required: true, default: "no-reply@example.com" },
66
+ mailer_sign_up_enabled: { type: [TrueClass, FalseClass], required: true, default: true },
67
+ mailer_sign_up_subject: { type: String, required: "mailer_sign_up_enabled", default: "SECURIAL: Welcome to Our Service" },
68
+ mailer_sign_in_enabled: { type: [TrueClass, FalseClass], required: true, default: true },
69
+ mailer_sign_in_subject: { type: String, required: "mailer_sign_in_enabled", default: "SECURIAL: Sign In Notification" },
70
+ mailer_update_account_enabled: { type: [TrueClass, FalseClass], required: true, default: true },
71
+ mailer_update_account_subject: { type: String, required: "mailer_update_account_enabled", default: "SECURIAL: Account Update Notification" },
72
+ mailer_forgot_password_subject: { type: String, required: true, default: "SECURIAL: Password Reset Instructions" },
73
+ }
74
+ end
75
+
76
+ def password_signature
77
+ {
78
+ password_min_length: { type: Numeric, required: true, default: 8 },
79
+ password_max_length: { type: Numeric, required: true, default: 128 },
80
+ password_complexity: { type: Regexp, required: true, default: Securial::Helpers::RegexHelper::PASSWORD_REGEX },
81
+ password_expires: { type: [TrueClass, FalseClass], required: true, default: true },
82
+ password_expires_in: { type: ActiveSupport::Duration, required: "password_expires", default: 90.days },
83
+ reset_password_token_expires_in: { type: ActiveSupport::Duration, required: true, default: 2.hours },
84
+ reset_password_token_secret: { type: String, required: true, default: "reset_secret" },
85
+ }
86
+ end
87
+
88
+ def response_signature
89
+ {
90
+ response_keys_format: { type: Symbol, required: true, allowed_values:
91
+ RESPONSE_KEYS_FORMATS, default: :snake_case, },
92
+ timestamps_in_response: { type: Symbol, required: true, allowed_values: TIMESTAMP_OPTIONS, default: :all },
93
+ }
94
+ end
95
+
96
+ def security_signature
97
+ {
98
+ security_headers: { type: Symbol, required: true, allowed_values: SECURITY_HEADERS, default: :strict },
99
+ rate_limiting_enabled: { type: [TrueClass, FalseClass], required: true, default: true },
100
+ rate_limit_requests_per_minute: { type: Numeric, required: "rate_limiting_enabled", default: 60 },
101
+ rate_limit_response_status: { type: Numeric, required: "rate_limiting_enabled", default: 429 },
102
+ rate_limit_response_message: { type: String, required: "rate_limiting_enabled", default: "Too many requests, please try again later." },
103
+ }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -1,24 +1,68 @@
1
1
  require "securial/logger"
2
- require "securial/config/validation/logger_validation"
3
- require "securial/config/validation/roles_validation"
4
- require "securial/config/validation/session_validation"
5
- require "securial/config/validation/mailer_validation"
6
- require "securial/config/validation/password_validation"
7
- require "securial/config/validation/response_validation"
8
- require "securial/config/validation/security_validation"
9
2
 
10
3
  module Securial
11
4
  module Config
12
5
  module Validation
13
6
  class << self
14
7
  def validate_all!(securial_config)
15
- Securial::Config::Validation::LoggerValidation.validate!(securial_config)
16
- Securial::Config::Validation::RolesValidation.validate!(securial_config)
17
- Securial::Config::Validation::SessionValidation.validate!(securial_config)
18
- Securial::Config::Validation::MailerValidation.validate!(securial_config)
19
- Securial::Config::Validation::PasswordValidation.validate!(securial_config)
20
- Securial::Config::Validation::ResponseValidation.validate!(securial_config)
21
- Securial::Config::Validation::SecurityValidation.validate!(securial_config)
8
+ signature = Securial::Config::Signature.config_signature
9
+
10
+ validate_required_fields!(signature, securial_config)
11
+ validate_types_and_values!(signature, securial_config)
12
+ validate_password_lengths!(securial_config)
13
+ end
14
+
15
+ private
16
+
17
+ def validate_required_fields!(signature, config)
18
+ signature.each do |key, options|
19
+ value = config.send(key)
20
+ required = options[:required]
21
+ if required == true && value.nil?
22
+ raise_error("#{key} is required but not provided.")
23
+ elsif required.is_a?(String)
24
+ dynamic_required = config.send(required)
25
+ signature[key][:required] = dynamic_required
26
+ if dynamic_required && value.nil?
27
+ raise_error("#{key} is required but not provided when #{required} is true.")
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def validate_types_and_values!(signature, config)
34
+ signature.each do |key, options|
35
+ next unless signature[key][:required]
36
+ value = config.send(key)
37
+ types = Array(options[:type])
38
+
39
+ unless types.any? { |type| value.is_a?(type) }
40
+ raise_error("#{key} must be of type(s) #{types.join(', ')}, but got #{value.class}.")
41
+ end
42
+
43
+ if options[:type] == ActiveSupport::Duration && value <= 0
44
+ raise_error("#{key} must be a positive duration, but got #{value}.")
45
+ end
46
+
47
+ if options[:type] == Numeric && value < 0
48
+ raise_error("#{key} must be a non-negative numeric value, but got #{value}.")
49
+ end
50
+
51
+ if options[:allowed_values] && options[:allowed_values].exclude?(value)
52
+ raise_error("#{key} must be one of #{options[:allowed_values].join(', ')}, but got #{value}.")
53
+ end
54
+ end
55
+ end
56
+
57
+ def validate_password_lengths!(config)
58
+ if config.password_min_length > config.password_max_length
59
+ raise_error("password_min_length cannot be greater than password_max_length.")
60
+ end
61
+ end
62
+
63
+ def raise_error(msg)
64
+ Securial.logger.fatal msg
65
+ raise Securial::Error::Config::InvalidConfigurationError, msg
22
66
  end
23
67
  end
24
68
  end
@@ -1,6 +1,8 @@
1
1
  require "securial/config/configuration"
2
+ require "securial/config/signature"
2
3
  require "securial/config/validation"
3
4
 
5
+
4
6
  module Securial
5
7
  class << self
6
8
  attr_accessor :configuration
@@ -1,8 +1,10 @@
1
1
  require "securial/auth"
2
2
  require "securial/config"
3
+ require "securial/error"
3
4
  require "securial/helpers"
4
5
  require "securial/logger"
5
6
  require "securial/middleware"
7
+ require "securial/security"
6
8
 
7
9
  module Securial
8
10
  class Engine < ::Rails::Engine
@@ -5,7 +5,7 @@ module Securial
5
5
  :password,
6
6
  :password_confirmation,
7
7
  :password_reset_token,
8
- :reset_password_token
8
+ :reset_password_token,
9
9
  ]
10
10
  end
11
11
 
@@ -16,10 +16,29 @@ module Securial
16
16
  end
17
17
  end
18
18
 
19
- initializer "securial.logger_middleware" do |app|
19
+ initializer "securial.security.request_rate_limiter" do |app|
20
+ if Securial.configuration.rate_limiting_enabled
21
+ Securial::Security::RequestRateLimiter.apply!
22
+ app.middleware.use Rack::Attack
23
+ end
24
+ end
25
+
26
+ initializer "securial.middleware.transform_request_keys" do |app|
27
+ app.middleware.use Securial::Middleware::TransformRequestKeys
28
+ end
29
+
30
+ initializer "securial.middleware.transform_response_keys" do |app|
31
+ app.middleware.use Securial::Middleware::TransformResponseKeys
32
+ end
33
+
34
+ initializer "securial.middleware.logger" do |app|
20
35
  app.middleware.use Securial::Middleware::RequestTagLogger
21
36
  end
22
37
 
38
+ initializer "securial.middleware.response_headers" do |app|
39
+ app.middleware.use Securial::Middleware::ResponseHeaders
40
+ end
41
+
23
42
  initializer "securial.extend_application_controller" do
24
43
  ActiveSupport.on_load(:action_controller_base) { include Securial::Identity }
25
44
  ActiveSupport.on_load(:action_controller_api) { include Securial::Identity }
@@ -4,34 +4,6 @@ module Securial
4
4
  class InvalidConfigurationError < Securial::Error::BaseError
5
5
  default_message "Invalid configuration for Securial"
6
6
  end
7
-
8
- class LoggerValidationError < InvalidConfigurationError
9
- default_message "Logger configuration validation failed"
10
- end
11
-
12
- class RolesValidationError < InvalidConfigurationError
13
- default_message "Roles configuration validation failed"
14
- end
15
-
16
- class SessionValidationError < InvalidConfigurationError
17
- default_message "Session configuration validation failed"
18
- end
19
-
20
- class MailerValidationError < InvalidConfigurationError
21
- default_message "Mailer configuration validation failed"
22
- end
23
-
24
- class PasswordValidationError < InvalidConfigurationError
25
- default_message "Password configuration validation failed"
26
- end
27
-
28
- class ResponseValidationError < InvalidConfigurationError
29
- default_message "Response configuration validation failed"
30
- end
31
-
32
- class SecurityValidationError < InvalidConfigurationError
33
- default_message "Security configuration validation failed"
34
- end
35
7
  end
36
8
  end
37
9
  end
@@ -0,0 +1,33 @@
1
+ module Securial
2
+ module Helpers
3
+ module KeyTransformer
4
+ module_function
5
+
6
+ def camelize(str, format)
7
+ case format
8
+ when :lowerCamelCase
9
+ str.to_s.camelize(:lower)
10
+ when :UpperCamelCase
11
+ str.to_s.camelize
12
+ else
13
+ str.to_s
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
33
+ end
@@ -1,6 +1,7 @@
1
1
  require "securial/helpers/normalizing_helper"
2
2
  require "securial/helpers/regex_helper"
3
3
  require "securial/helpers/roles_helper"
4
+ require "securial/helpers/key_transformer"
4
5
 
5
6
  module Securial
6
7
  module Helpers
@@ -0,0 +1,19 @@
1
+ module Securial
2
+ module Middleware
3
+ # This middleware removes sensitive headers from the request environment.
4
+ # It is designed to enhance security by ensuring that sensitive information
5
+ # is not inadvertently logged or processed.
6
+ class ResponseHeaders
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ status, headers, response = @app.call(env)
13
+ headers["X-Securial-Mounted"] = "true"
14
+ headers["X-Securial-Developer"] = "Aly Badawy - https://alybadawy.com | @alybadawy"
15
+ [status, headers, response]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ module Securial
2
+ module Middleware
3
+ # This middleware transforms request keys to a specified format.
4
+ # It uses the KeyTransformer helper to apply the transformation.
5
+ #
6
+ # It reads the request body if the content type is JSON and transforms
7
+ # the keys to underscore format. If the body is not valid JSON, it does nothing.
8
+ class TransformRequestKeys
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ if env["CONTENT_TYPE"]&.include?("application/json")
15
+ req = Rack::Request.new(env)
16
+ if (req.body&.size || 0) > 0
17
+ raw = req.body.read
18
+ req.body.rewind
19
+ begin
20
+ parsed = JSON.parse(raw)
21
+ transformed = Securial::Helpers::KeyTransformer.deep_transform_keys(parsed) do |key|
22
+ Securial::Helpers::KeyTransformer.underscore(key)
23
+ end
24
+ env["rack.input"] = StringIO.new(JSON.dump(transformed))
25
+ env["rack.input"].rewind
26
+ rescue JSON::ParserError
27
+ # no-op
28
+ end
29
+ end
30
+ end
31
+ @app.call(env)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ module Securial
2
+ module Middleware
3
+ # This middleware transforms response keys to a specified format.
4
+ # It uses the KeyTransformer helper to apply the transformation.
5
+ #
6
+ # It reads the response body if the content type is JSON and transforms
7
+ # the keys to the specified format (default is lowerCamelCase).
8
+ class TransformResponseKeys
9
+ def initialize(app, format: :lowerCamelCase)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ status, headers, response = @app.call(env)
15
+
16
+ if json_response?(headers)
17
+ body = extract_body(response)
18
+
19
+ if body.present?
20
+ format = Securial.configuration.response_keys_format
21
+ transformed = Securial::Helpers::KeyTransformer.deep_transform_keys(JSON.parse(body)) do |key|
22
+ Securial::Helpers::KeyTransformer.camelize(key, format)
23
+ end
24
+
25
+ new_body = [JSON.generate(transformed)]
26
+ headers["Content-Length"] = new_body.first.bytesize.to_s
27
+ return [status, headers, new_body]
28
+ end
29
+ end
30
+
31
+ [status, headers, response]
32
+ end
33
+
34
+ private
35
+
36
+ def json_response?(headers)
37
+ headers["Content-Type"]&.include?("application/json")
38
+ end
39
+
40
+ def extract_body(response)
41
+ response_body = ""
42
+ response.each { |part| response_body << part }
43
+ response_body
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,4 +1,7 @@
1
1
  require "securial/middleware/request_tag_logger"
2
+ require "securial/middleware/transform_request_keys"
3
+ require "securial/middleware/transform_response_keys"
4
+ require "securial/middleware/response_headers"
2
5
 
3
6
  module Securial
4
7
  module Middleware
@@ -0,0 +1,45 @@
1
+ require "rack/attack"
2
+ require "securial/config"
3
+
4
+ module Securial
5
+ module Security
6
+ module RequestRateLimiter
7
+ module_function
8
+
9
+ def apply! # rubocop:disable Metrics/MethodLength
10
+ resp_status = Securial.configuration.rate_limit_response_status
11
+ resp_message = Securial.configuration.rate_limit_response_message
12
+ throttle_configs = [
13
+ { name: "securial/logins/ip", path: "sessions/login", key: ->(req) { req.ip } },
14
+ { name: "securial/logins/email", path: "sessions/login", key: ->(req) { req.params["email_address"].to_s.downcase.strip } },
15
+ { name: "securial/password_resets/ip", path: "password/forgot", key: ->(req) { req.ip } },
16
+ { name: "securial/password_resets/email", path: "password/forgot", key: ->(req) { req.params["email_address"].to_s.downcase.strip } },
17
+ ]
18
+
19
+ throttle_configs.each do |config|
20
+ Rack::Attack.throttle(config[:name],
21
+ limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
22
+ period: 1.minute
23
+ ) do |req|
24
+ if req.path.include?(config[:path]) && req.post?
25
+ config[:key].call(req)
26
+ end
27
+ end
28
+ end
29
+
30
+ # Custom response for throttled requests
31
+ Rack::Attack.throttled_responder = lambda do |request|
32
+ retry_after = (request.env["rack.attack.match_data"] || {})[:period]
33
+ [
34
+ resp_status,
35
+ {
36
+ "Content-Type" => "application/json",
37
+ "Retry-After" => retry_after.to_s,
38
+ },
39
+ [{ error: resp_message }.to_json],
40
+ ]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ 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.8.1".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
@@ -0,0 +1,26 @@
1
+ namespace :securial do
2
+ desc "Print all routes for the Securial engine"
3
+ task routes: :environment do
4
+ engine = Securial::Engine
5
+ routes = engine.routes
6
+
7
+ puts "Routes for Securial::Engine:"
8
+ all_routes = routes.routes.map do |route|
9
+ {
10
+ verb: route.verb,
11
+ path: route.path.spec.to_s.gsub("(.:format)", ""),
12
+ name: route.name,
13
+ controller: route.defaults[:controller],
14
+ action: route.defaults[:action],
15
+ }
16
+ end
17
+
18
+ # Print in a table format with adjusted column widths
19
+ puts "%-10s %-35s %-35s %-30s" % ["Verb", "Path", "Name", "Controller#Action"]
20
+ puts "-" * 110
21
+ all_routes.each do |r|
22
+ ctrl_action = [r[:controller], r[:action]].compact.join("#")
23
+ puts "%-10s %-35s %-35s %-30s" % [r[:verb], r[:path], r[:name], ctrl_action]
24
+ end
25
+ end
26
+ end