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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +90 -0
- data/Rakefile +8 -0
- data/app/controllers/concerns/securial/identity.rb +67 -0
- data/app/controllers/securial/accounts_controller.rb +60 -0
- data/app/controllers/securial/application_controller.rb +18 -0
- data/app/controllers/securial/passwords_controller.rb +35 -0
- data/app/controllers/securial/role_assignments_controller.rb +49 -0
- data/app/controllers/securial/roles_controller.rb +44 -0
- data/app/controllers/securial/sessions_controller.rb +76 -0
- data/app/controllers/securial/status_controller.rb +9 -0
- data/app/controllers/securial/users_controller.rb +53 -0
- data/app/jobs/securial/application_job.rb +4 -0
- data/app/mailers/securial/application_mailer.rb +6 -0
- data/app/mailers/securial/securial_mailer.rb +17 -0
- data/app/models/concerns/securial/password_resettable.rb +47 -0
- data/app/models/securial/application_record.rb +18 -0
- data/app/models/securial/current.rb +6 -0
- data/app/models/securial/role.rb +10 -0
- data/app/models/securial/role_assignment.rb +6 -0
- data/app/models/securial/session.rb +27 -0
- data/app/models/securial/user.rb +54 -0
- data/app/views/layouts/securial/mailer.html.erb +13 -0
- data/app/views/layouts/securial/mailer.text.erb +1 -0
- data/app/views/securial/accounts/show.json.jbuilder +1 -0
- data/app/views/securial/passwords/_password.json.jbuilder +2 -0
- data/app/views/securial/passwords/index.json.jbuilder +1 -0
- data/app/views/securial/passwords/show.json.jbuilder +1 -0
- data/app/views/securial/role_assignments/show.json.jbuilder +1 -0
- data/app/views/securial/roles/_securial_role.json.jbuilder +9 -0
- data/app/views/securial/roles/index.json.jbuilder +6 -0
- data/app/views/securial/roles/show.json.jbuilder +1 -0
- data/app/views/securial/securial_mailer/reset_password.html.erb +5 -0
- data/app/views/securial/securial_mailer/reset_password.text.erb +4 -0
- data/app/views/securial/sessions/_session.json.jbuilder +15 -0
- data/app/views/securial/sessions/index.json.jbuilder +6 -0
- data/app/views/securial/sessions/show.json.jbuilder +1 -0
- data/app/views/securial/status/show.json.jbuilder +3 -0
- data/app/views/securial/users/_securial_user.json.jbuilder +14 -0
- data/app/views/securial/users/index.json.jbuilder +6 -0
- data/app/views/securial/users/show.json.jbuilder +1 -0
- data/bin/securial +58 -0
- data/config/routes.rb +41 -0
- data/db/migrate/20250515104930_create_securial_roles.rb +12 -0
- data/db/migrate/20250517155521_create_securial_users.rb +18 -0
- data/db/migrate/20250518122749_create_securial_role_assignments.rb +10 -0
- data/db/migrate/20250519075407_create_securial_sessions.rb +15 -0
- data/db/migrate/20250524210207_add_password_reset_fields_to_securial_users.rb +6 -0
- data/lib/generators/factory_bot/model/model_generator.rb +31 -0
- data/lib/generators/factory_bot/templates/factory.erb +7 -0
- data/lib/generators/securial/install/install_generator.rb +37 -0
- data/lib/generators/securial/install/templates/securial_initializer.erb +109 -0
- data/lib/generators/securial/jbuilder/jbuilder_generator.rb +52 -0
- data/lib/generators/securial/jbuilder/templates/_resource.json.erb +10 -0
- data/lib/generators/securial/jbuilder/templates/index.json.erb +7 -0
- data/lib/generators/securial/jbuilder/templates/show.json.erb +1 -0
- data/lib/generators/securial/scaffold/scaffold_generator.rb +146 -0
- data/lib/generators/securial/scaffold/templates/controller.erb +44 -0
- data/lib/generators/securial/scaffold/templates/request_spec.erb +61 -0
- data/lib/generators/securial/scaffold/templates/routes.erb +11 -0
- data/lib/generators/securial/scaffold/templates/routing_spec.erb +31 -0
- data/lib/securial/configuration.rb +35 -0
- data/lib/securial/engine.rb +89 -0
- data/lib/securial/errors/config_errors.rb +12 -0
- data/lib/securial/errors/session_errors.rb +6 -0
- data/lib/securial/factories/securial/role_assignments.rb +6 -0
- data/lib/securial/factories/securial/roles.rb +18 -0
- data/lib/securial/factories/securial/sessions.rb +12 -0
- data/lib/securial/factories/securial/users.rb +17 -0
- data/lib/securial/helpers/auth_helper.rb +46 -0
- data/lib/securial/helpers/normalizing_helper.rb +17 -0
- data/lib/securial/helpers/regex_helper.rb +17 -0
- data/lib/securial/logger.rb +71 -0
- data/lib/securial/middleware/request_logger_tag.rb +18 -0
- data/lib/securial/route_inspector.rb +50 -0
- data/lib/securial/version.rb +3 -0
- data/lib/securial.rb +94 -0
- data/lib/tasks/securial_tasks.rake +4 -0
- 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,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
|