securial 0.4.2

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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +90 -0
  4. data/Rakefile +8 -0
  5. data/app/controllers/concerns/securial/identity.rb +67 -0
  6. data/app/controllers/securial/accounts_controller.rb +60 -0
  7. data/app/controllers/securial/application_controller.rb +18 -0
  8. data/app/controllers/securial/passwords_controller.rb +35 -0
  9. data/app/controllers/securial/role_assignments_controller.rb +49 -0
  10. data/app/controllers/securial/roles_controller.rb +44 -0
  11. data/app/controllers/securial/sessions_controller.rb +76 -0
  12. data/app/controllers/securial/status_controller.rb +9 -0
  13. data/app/controllers/securial/users_controller.rb +53 -0
  14. data/app/jobs/securial/application_job.rb +4 -0
  15. data/app/mailers/securial/application_mailer.rb +6 -0
  16. data/app/mailers/securial/securial_mailer.rb +17 -0
  17. data/app/models/concerns/securial/password_resettable.rb +47 -0
  18. data/app/models/securial/application_record.rb +18 -0
  19. data/app/models/securial/current.rb +6 -0
  20. data/app/models/securial/role.rb +10 -0
  21. data/app/models/securial/role_assignment.rb +6 -0
  22. data/app/models/securial/session.rb +27 -0
  23. data/app/models/securial/user.rb +54 -0
  24. data/app/views/layouts/securial/mailer.html.erb +13 -0
  25. data/app/views/layouts/securial/mailer.text.erb +1 -0
  26. data/app/views/securial/accounts/show.json.jbuilder +1 -0
  27. data/app/views/securial/passwords/_password.json.jbuilder +2 -0
  28. data/app/views/securial/passwords/index.json.jbuilder +1 -0
  29. data/app/views/securial/passwords/show.json.jbuilder +1 -0
  30. data/app/views/securial/role_assignments/show.json.jbuilder +1 -0
  31. data/app/views/securial/roles/_securial_role.json.jbuilder +9 -0
  32. data/app/views/securial/roles/index.json.jbuilder +6 -0
  33. data/app/views/securial/roles/show.json.jbuilder +1 -0
  34. data/app/views/securial/securial_mailer/reset_password.html.erb +5 -0
  35. data/app/views/securial/securial_mailer/reset_password.text.erb +4 -0
  36. data/app/views/securial/sessions/_session.json.jbuilder +15 -0
  37. data/app/views/securial/sessions/index.json.jbuilder +6 -0
  38. data/app/views/securial/sessions/show.json.jbuilder +1 -0
  39. data/app/views/securial/status/show.json.jbuilder +3 -0
  40. data/app/views/securial/users/_securial_user.json.jbuilder +14 -0
  41. data/app/views/securial/users/index.json.jbuilder +6 -0
  42. data/app/views/securial/users/show.json.jbuilder +1 -0
  43. data/bin/securial +58 -0
  44. data/config/routes.rb +41 -0
  45. data/db/migrate/20250515104930_create_securial_roles.rb +12 -0
  46. data/db/migrate/20250517155521_create_securial_users.rb +18 -0
  47. data/db/migrate/20250518122749_create_securial_role_assignments.rb +10 -0
  48. data/db/migrate/20250519075407_create_securial_sessions.rb +15 -0
  49. data/db/migrate/20250524210207_add_password_reset_fields_to_securial_users.rb +6 -0
  50. data/lib/generators/factory_bot/model/model_generator.rb +31 -0
  51. data/lib/generators/factory_bot/templates/factory.erb +7 -0
  52. data/lib/generators/securial/install/install_generator.rb +37 -0
  53. data/lib/generators/securial/install/templates/securial_initializer.erb +109 -0
  54. data/lib/generators/securial/jbuilder/jbuilder_generator.rb +52 -0
  55. data/lib/generators/securial/jbuilder/templates/_resource.json.erb +10 -0
  56. data/lib/generators/securial/jbuilder/templates/index.json.erb +7 -0
  57. data/lib/generators/securial/jbuilder/templates/show.json.erb +1 -0
  58. data/lib/generators/securial/scaffold/scaffold_generator.rb +146 -0
  59. data/lib/generators/securial/scaffold/templates/controller.erb +44 -0
  60. data/lib/generators/securial/scaffold/templates/request_spec.erb +61 -0
  61. data/lib/generators/securial/scaffold/templates/routes.erb +11 -0
  62. data/lib/generators/securial/scaffold/templates/routing_spec.erb +31 -0
  63. data/lib/securial/configuration.rb +35 -0
  64. data/lib/securial/engine.rb +89 -0
  65. data/lib/securial/errors/config_errors.rb +12 -0
  66. data/lib/securial/errors/session_errors.rb +6 -0
  67. data/lib/securial/factories/securial/role_assignments.rb +6 -0
  68. data/lib/securial/factories/securial/roles.rb +18 -0
  69. data/lib/securial/factories/securial/sessions.rb +12 -0
  70. data/lib/securial/factories/securial/users.rb +17 -0
  71. data/lib/securial/helpers/auth_helper.rb +46 -0
  72. data/lib/securial/helpers/normalizing_helper.rb +17 -0
  73. data/lib/securial/helpers/regex_helper.rb +17 -0
  74. data/lib/securial/logger.rb +71 -0
  75. data/lib/securial/middleware/request_logger_tag.rb +18 -0
  76. data/lib/securial/route_inspector.rb +50 -0
  77. data/lib/securial/version.rb +3 -0
  78. data/lib/securial.rb +94 -0
  79. data/lib/tasks/securial_tasks.rake +4 -0
  80. metadata +435 -0
@@ -0,0 +1,146 @@
1
+ require "rails/generators"
2
+ require "rails/generators/resource_helpers"
3
+ require "rails/generators/named_base"
4
+
5
+ module Securial
6
+ module Generators
7
+ class ScaffoldGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::ResourceHelpers
9
+
10
+ allow_incompatible_default_type!
11
+
12
+ Thor::Base.shell = Thor::Shell::Color
13
+
14
+ source_root File.expand_path("templates", __dir__)
15
+
16
+ class_option :orm,
17
+ type: :string,
18
+ default: "active_record",
19
+ desc: "ORM to use (defaults to active_record)",
20
+ banner: "NAME"
21
+
22
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
23
+
24
+ def self.file_name
25
+ "generator_manifest.txt"
26
+ end
27
+
28
+ def run_scaffold # rubocop:disable Metrics/MethodLength
29
+ say_status(:scaffold, "Running built-in scaffold generator with custom options", :cyan) unless Rails.env.test?
30
+
31
+ # Generate model and migration
32
+ Rails::Generators.invoke(
33
+ "model",
34
+ [name, *attributes.map(&:to_s)],
35
+ behavior: behavior,
36
+ destination_root: root_path,
37
+ )
38
+
39
+ indent = " " # two-space indent
40
+
41
+ # Generate controller from template
42
+ say_status(:scaffold, "controller", :cyan) unless Rails.env.test?
43
+ say_status(status_behavior, "#{indent}#{controller_path}", status_color) unless Rails.env.test?
44
+ template("controller.erb", controller_path, verbose: false)
45
+
46
+ # Generate views
47
+ say_status(:scaffold, "JBuilder views", :cyan) unless Rails.env.test?
48
+ Rails::Generators.invoke(
49
+ "securial:jbuilder",
50
+ [name, *attributes.map(&:to_s)],
51
+ behavior: behavior,
52
+ destination_root: Securial::Engine.root,
53
+ )
54
+
55
+ add_routes
56
+
57
+ say_status(:scaffold, "controller specs: #{@routing_spec_path}", :cyan) unless Rails.env.test?
58
+
59
+ # Generate request specs
60
+ say_status(status_behavior, "#{indent}#{request_spec_path}", status_color) unless Rails.env.test?
61
+ template("request_spec.erb", request_spec_path, verbose: false)
62
+
63
+ # Generate routing specs
64
+ say_status(status_behavior, "#{indent}#{routing_spec_path}", status_color) unless Rails.env.test?
65
+ template("routing_spec.erb", routing_spec_path, verbose: false)
66
+
67
+ say_status(:success, "Scaffold complete ✨✨", :green) unless Rails.env.test?
68
+ end
69
+
70
+ private
71
+
72
+ def namespaced_name
73
+ @namespaced_name ||= "securial_#{name.underscore}"
74
+ end
75
+
76
+ def plain_plural_name
77
+ @plain_plural_name ||= name.underscore.pluralize
78
+ end
79
+
80
+ def resource_path_name
81
+ plain_plural_name
82
+ end
83
+
84
+ def table_name
85
+ @table_name ||= namespaced_name.pluralize
86
+ end
87
+
88
+ def controller_class_name
89
+ class_name.pluralize
90
+ end
91
+
92
+ def controller_file_name
93
+ plain_plural_name
94
+ end
95
+
96
+ def status_behavior
97
+ behavior == :invoke ? :create : :remove
98
+ end
99
+
100
+ def status_color
101
+ behavior == :invoke ? :green : :red
102
+ end
103
+
104
+ def controller_path
105
+ base_path = root_path.join("app/controllers/securial")
106
+ @controller_path ||= File.join(base_path, "#{controller_file_name}_controller.rb")
107
+ end
108
+
109
+ def request_spec_path
110
+ base_path = root_path.join("spec/requests/securial")
111
+ @request_spec_path ||= File.join(base_path, "#{resource_path_name}_spec.rb")
112
+ end
113
+
114
+ def routing_spec_path
115
+ base_path = root_path.join("spec/routing/securial")
116
+ @routing_spec_path ||= File.join(base_path, "#{resource_path_name}_routing_spec.rb")
117
+ end
118
+
119
+ def root_path
120
+ Rails.env.test? ? Rails.root.join("tmp/") : Engine.root
121
+ end
122
+
123
+ def add_routes
124
+ say_status(:routes, "routes", :cyan) unless Rails.env.test?
125
+
126
+ routes_path = File.join(root_path, "config/routes.rb")
127
+
128
+ route_config = "resources :#{plain_plural_name}"
129
+ template("routes.erb", routes_path, verbose: false) unless File.exist?(routes_path)
130
+
131
+ if behavior == :invoke
132
+ inject_into_file routes_path,
133
+ " #{route_config}\n",
134
+ after: "defaults format: :json do\n",
135
+ verbose: !Rails.env.test?
136
+ else
137
+ content = File.read(routes_path)
138
+
139
+ # Remove the route line and write back
140
+ new_content = content.gsub(/^\s*#{Regexp.escape(route_config)}\n/, "")
141
+ File.write(routes_path, new_content)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,44 @@
1
+ module Securial
2
+ class <%= controller_class_name %>Controller < ApplicationController
3
+ before_action :set_<%= singular_table_name %>, only: [:show, :update, :destroy]
4
+
5
+ def index
6
+ @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
7
+ end
8
+
9
+ def show
10
+ end
11
+
12
+ def create
13
+ @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
14
+
15
+ if @<%= singular_table_name %>.save
16
+ render :show, status: :created, location: @<%= singular_table_name %>
17
+ else
18
+ render json: @<%= singular_table_name %>.errors, status: :unprocessable_entity
19
+ end
20
+ end
21
+
22
+ def update
23
+ if @<%= singular_table_name %>.update(<%= singular_table_name %>_params)
24
+ render :show
25
+ else
26
+ render json: @<%= singular_table_name %>.errors, status: :unprocessable_entity
27
+ end
28
+ end
29
+
30
+ def destroy
31
+ @<%= singular_table_name %>.destroy
32
+ end
33
+
34
+ private
35
+
36
+ def set_<%= singular_table_name %>
37
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
38
+ end
39
+
40
+ def <%= singular_table_name %>_params
41
+ params.expect(<%= singular_table_name %>: [<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>])
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,61 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe "/<%= plural_table_name %>", type: :request do
4
+ let(:<%= singular_table_name %>) { create(:<%= singular_table_name %>) }
5
+
6
+ describe "GET /index" do
7
+ it "returns http success" do
8
+ get securial.<%= class_name.pluralize.downcase %>_path
9
+ expect(response).to have_http_status(:success)
10
+ end
11
+ end
12
+
13
+ describe "GET /show" do
14
+ it "returns http success" do
15
+ get securial.<%= class_name.downcase %>_path(<%= singular_table_name %>)
16
+ expect(response).to have_http_status(:success)
17
+ end
18
+ end
19
+
20
+ describe "POST /create" do
21
+ context "with valid parameters" do
22
+ let(:valid_attributes) {
23
+ attributes_for(:<%= singular_table_name %>)
24
+ }
25
+
26
+ it "creates a new <%= class_name %>" do
27
+ expect {
28
+ post securial.<%= class_name.pluralize.downcase %>_path, params: { <%= singular_table_name %>: valid_attributes }
29
+ }.to change(Securial::<%= class_name %>, :count).by(1)
30
+ end
31
+
32
+ it "returns http created" do
33
+ post securial.<%= class_name.pluralize.downcase %>_path, params: { <%= singular_table_name %>: valid_attributes }
34
+ expect(response).to have_http_status(:created)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "PUT /update" do
40
+ context "with valid parameters" do
41
+ let(:new_attributes) {
42
+ attributes_for(:<%= singular_table_name %>)
43
+ }
44
+
45
+ it "updates the requested <%= singular_table_name %>" do
46
+ put securial.<%= class_name.downcase %>_path(<%= singular_table_name %>), params: { <%= singular_table_name %>: new_attributes }
47
+ <%= singular_table_name %>.reload
48
+ expect(response).to have_http_status(:ok)
49
+ end
50
+ end
51
+ end
52
+
53
+ describe "DELETE /destroy" do
54
+ it "destroys the requested <%= singular_table_name %>" do
55
+ <%= singular_table_name %> # Create the record
56
+ expect {
57
+ delete securial.<%= class_name.downcase %>_path(<%= singular_table_name %>)
58
+ }.to change(Securial::<%= class_name %>, :count).by(-1)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,11 @@
1
+ Securial::Engine.routes.draw do
2
+ defaults format: :json do
3
+ get "/status", to: "status#show", as: :status
4
+
5
+ namespace Securial.admin_namespace do
6
+ # Placeholder for admin-specific routes. Add routes here as needed.
7
+ # For example:
8
+ # resources :users, only: [:index, :show]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Securial::<%= controller_class_name %>Controller, type: :routing do
4
+ routes { Securial::Engine.routes }
5
+
6
+ describe "routing" do
7
+ it "routes to #index" do
8
+ expect(get: "/<%= class_name.pluralize.downcase %>").to route_to("securial/<%= class_name.pluralize.downcase %>#index", format: :json)
9
+ end
10
+
11
+ it "routes to #show" do
12
+ expect(get: "/<%= class_name.pluralize.downcase %>/1").to route_to("securial/<%= class_name.pluralize.downcase %>#show", id: "1", format: :json)
13
+ end
14
+
15
+ it "routes to #create" do
16
+ expect(post: "/<%= class_name.pluralize.downcase %>").to route_to("securial/<%= class_name.pluralize.downcase %>#create", format: :json)
17
+ end
18
+
19
+ it "routes to #update via PUT" do
20
+ expect(put: "/<%= class_name.pluralize.downcase %>/1").to route_to("securial/<%= class_name.pluralize.downcase %>#update", id: "1", format: :json)
21
+ end
22
+
23
+ it "routes to #update via PATCH" do
24
+ expect(patch: "/<%= class_name.pluralize.downcase %>/1").to route_to("securial/<%= class_name.pluralize.downcase %>#update", id: "1", format: :json)
25
+ end
26
+
27
+ it "routes to #destroy" do
28
+ expect(delete: "/<%= class_name.pluralize.downcase %>/1").to route_to("securial/<%= class_name.pluralize.downcase %>#destroy", id: "1", format: :json)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ module Securial
2
+ class Configuration
3
+ attr_accessor :log_to_file, :log_to_stdout
4
+ attr_accessor :log_file_level, :log_stdout_level
5
+ attr_accessor :admin_role
6
+ attr_accessor :session_expiration_duration
7
+ attr_accessor :session_secret, :session_algorithm
8
+ attr_accessor :mailer_sender
9
+ attr_accessor :password_reset_email_subject
10
+ attr_accessor :password_min_length, :password_max_length
11
+ attr_accessor :password_complexity
12
+ attr_accessor :password_expires_in
13
+ attr_accessor :reset_password_token_expires_in
14
+ attr_accessor :reset_password_token_secret
15
+
16
+ def initialize
17
+ @log_to_file = !Rails.env.test?
18
+ @log_to_stdout = !Rails.env.test?
19
+ @log_file_level = :info
20
+ @log_stdout_level = :info
21
+ @admin_role = :admin
22
+ @session_expiration_duration = 3.minutes
23
+ @session_secret = "secret"
24
+ @session_algorithm = :hs256
25
+ @mailer_sender = "no-reply@example.com"
26
+ @password_reset_email_subject = "SECURIAL: Password Reset Instructions"
27
+ @password_min_length = 8
28
+ @password_max_length = 128
29
+ @password_complexity = Securial::RegexHelper::PASSWORD_REGEX
30
+ @password_expires_in = 90.days
31
+ @reset_password_token_expires_in = 2.hours
32
+ @reset_password_token_secret = "reset_secret"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,89 @@
1
+ require_relative "./logger"
2
+
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"
14
+ require "jwt"
15
+
16
+ module Securial
17
+ class Engine < ::Rails::Engine
18
+ isolate_namespace Securial
19
+
20
+ initializer "securial.filter_parameters" do |app|
21
+ app.config.filter_parameters += [
22
+ :password,
23
+ :password_confirmation,
24
+ :password_reset_token,
25
+ :reset_password_token
26
+ ]
27
+ end
28
+
29
+ initializer "securial.logger" do
30
+ Securial.const_set(:ENGINE_LOGGER, Securial::Logger.build)
31
+ end
32
+
33
+ initializer "securial.engine_initialized" do |app|
34
+ Securial::ENGINE_LOGGER.info("[Securial] Engine mounted. Host app: #{app.class.name}")
35
+ end
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
41
+ end
42
+
43
+ initializer "securial.load_factory_bot_generator" do
44
+ require_relative "../generators/factory_bot/model/model_generator"
45
+ end
46
+
47
+ initializer "securial.extend_application_controller" do
48
+ ActiveSupport.on_load(:action_controller_base) do
49
+ include Securial::Identity
50
+ end
51
+
52
+ ActiveSupport.on_load(:action_controller_api) do
53
+ include Securial::Identity
54
+ end
55
+ end
56
+
57
+ config.generators.api_only = true
58
+
59
+ config.autoload_paths += Dir["#{config.root}/lib/generators"]
60
+
61
+ config.generators do |g|
62
+ g.orm :active_record, primary_key_type: :string
63
+
64
+ g.test_framework :rspec,
65
+ fixtures: false,
66
+ view_specs: false,
67
+ helper_specs: false,
68
+ routing_specs: true,
69
+ controller_specs: true,
70
+ request_specs: true,
71
+ model_specs: true
72
+
73
+ g.fixture_replacement :factory_bot, dir: "lib/securial/factories"
74
+
75
+ # Add JBuilder configuration
76
+ g.template_engine :jbuilder
77
+ end
78
+
79
+ initializer "securial.middleware" do |app|
80
+ app.middleware.use Securial::Middleware::RequestLoggerTag
81
+ end
82
+
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
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,12 @@
1
+ module Securial
2
+ module ConfigErrors
3
+ class ConfigAdminRoleError < StandardError; end
4
+
5
+ class ConfigSessionExpirationDurationError < StandardError; end
6
+ class ConfigSessionAlgorithmError < StandardError; end
7
+ class ConfigSessionSecretError < StandardError; end
8
+
9
+ class ConfigMailerSenderError < StandardError; end
10
+ class ConfigPasswordError < StandardError; end
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ module Securial
2
+ module SessionErrors
3
+ class SessionRevokedError < StandardError; end
4
+ class SessionExpiredError < StandardError; end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :securial_role_assignment, class: "Securial::RoleAssignment" do
3
+ association :user, factory: :securial_user
4
+ association :role, factory: :securial_role
5
+ end
6
+ end
@@ -0,0 +1,18 @@
1
+ FactoryBot.define do
2
+ factory :securial_role, class: "Securial::Role" do
3
+ role_name { "MyString" }
4
+ hide_from_profile { false }
5
+
6
+ trait :admin do
7
+ role_name { "Admin" }
8
+ end
9
+
10
+ trait :user do
11
+ role_name { "User" }
12
+ end
13
+
14
+ trait :hidden do
15
+ hide_from_profile { true }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ FactoryBot.define do
2
+ factory :securial_session, class: "Securial::Session" do
3
+ ip_address { "127.0.0.1" }
4
+ user_agent { "Ruby/RSpec" }
5
+ refresh_count { 1 }
6
+ refresh_token { SecureRandom.hex(64) }
7
+ last_refreshed_at { Time.current }
8
+ refresh_token_expires_at { 1.week.from_now }
9
+ revoked { false }
10
+ association :user, factory: :securial_user
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ FactoryBot.define do
2
+ factory :securial_user, class: "Securial::User" do
3
+ email_address { Faker::Internet.email }
4
+ password { "Password_.1" }
5
+ password_confirmation { "Password_.1" }
6
+ first_name { Faker::Name.first_name }
7
+ last_name { Faker::Name.last_name }
8
+ phone { Faker::PhoneNumber.cell_phone }
9
+ username { Faker::Internet.username(specifier: 3..20) }
10
+ bio { Faker::Lorem.paragraph }
11
+
12
+ trait :admin do
13
+ admin_role = Securial.configuration.admin_role.to_s.strip.titleize
14
+ roles { [Securial::Role.find_or_create_by(role_name: admin_role)] }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ module Securial
2
+ module AuthHelper
3
+ class << self
4
+ def encode(session)
5
+ return nil unless session && session.class == Securial::Session
6
+
7
+ base_payload = {
8
+ jti: session.id,
9
+ exp: expiry_duration.from_now.to_i,
10
+ sub: "session-access-token",
11
+ refresh_count: session.refresh_count,
12
+ }
13
+
14
+ session_payload = {
15
+ ip: session.ip_address,
16
+ agent: session.user_agent,
17
+ }
18
+
19
+ payload = base_payload.merge(session_payload)
20
+ JWT.encode(payload, secret, algorithm, { kid: "hmac" })
21
+ end
22
+
23
+ def decode(token)
24
+ decoded = JWT.decode(token, secret, true, { algorithm: algorithm, verify_jti: true, iss: "securial" })
25
+ decoded.first
26
+ end
27
+
28
+ private
29
+
30
+ def secret
31
+ Securial.validate_session_secret!
32
+ Securial.configuration.session_secret
33
+ end
34
+
35
+ def algorithm
36
+ Securial.validate_session_algorithm!
37
+ Securial.configuration.session_algorithm.to_s.upcase
38
+ end
39
+
40
+ def expiry_duration
41
+ Securial.validate_session_expiry_duration!
42
+ Securial.configuration.session_expiration_duration
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ module Securial
2
+ module NormalizingHelper
3
+ module_function
4
+
5
+ def normalize_email_address(email)
6
+ return "" if email.empty?
7
+
8
+ email.strip.downcase
9
+ end
10
+
11
+ def normalize_role_name(role_name)
12
+ return "" if role_name.empty?
13
+
14
+ role_name.strip.downcase.titleize
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
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}
6
+
7
+ module_function
8
+
9
+ def valid_email?(email)
10
+ email.match?(EMAIL_REGEX)
11
+ end
12
+
13
+ def valid_username?(username)
14
+ username.match?(USERNAME_REGEX)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,71 @@
1
+ require "logger"
2
+ require "active_support/logger"
3
+ require "active_support/tagged_logging"
4
+
5
+ module Securial
6
+ class Logger
7
+ def self.build
8
+ outputs = []
9
+
10
+ if Securial.configuration.log_to_file
11
+ log_file = Rails.root.join("log", "securial.log").open("a")
12
+ log_file.sync = true
13
+ outputs << log_file
14
+ end
15
+
16
+ if Securial.configuration.log_to_stdout
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
70
+ end
71
+ end
@@ -0,0 +1,18 @@
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