rodauth-rails 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +177 -77
- data/lib/generators/rodauth/install_generator.rb +28 -18
- data/lib/generators/rodauth/migration/account_expiration.erb +7 -0
- data/lib/generators/rodauth/migration/active_sessions.erb +7 -0
- data/lib/generators/rodauth/migration/audit_logging.erb +16 -0
- data/lib/generators/rodauth/migration/base.erb +19 -0
- data/lib/generators/rodauth/migration/disallow_password_reuse.erb +5 -0
- data/lib/generators/rodauth/migration/email_auth.erb +7 -0
- data/lib/generators/rodauth/migration/jwt_refresh.erb +7 -0
- data/lib/generators/rodauth/migration/lockout.erb +11 -0
- data/lib/generators/rodauth/migration/otp.erb +7 -0
- data/lib/generators/rodauth/migration/password_expiration.erb +5 -0
- data/lib/generators/rodauth/migration/recovery_codes.erb +6 -0
- data/lib/generators/rodauth/migration/remember.erb +6 -0
- data/lib/generators/rodauth/migration/reset_password.erb +7 -0
- data/lib/generators/rodauth/migration/single_session.erb +5 -0
- data/lib/generators/rodauth/migration/sms_codes.erb +8 -0
- data/lib/generators/rodauth/migration/verify_account.erb +7 -0
- data/lib/generators/rodauth/migration/verify_login_change.erb +7 -0
- data/lib/generators/rodauth/migration/webauthn.erb +12 -0
- data/lib/generators/rodauth/migration_generator.rb +32 -0
- data/lib/generators/rodauth/migration_helpers.rb +69 -0
- data/lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb +2 -1
- data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +18 -18
- data/lib/generators/rodauth/templates/config/initializers/sequel.rb +1 -5
- data/lib/generators/rodauth/templates/db/migrate/create_rodauth.rb +2 -176
- data/lib/rodauth/rails.rb +23 -4
- data/lib/rodauth/rails/app.rb +3 -1
- data/lib/rodauth/rails/app/flash.rb +1 -1
- data/lib/rodauth/rails/app/middleware.rb +26 -0
- data/lib/rodauth/rails/feature.rb +92 -25
- data/lib/rodauth/rails/railtie.rb +11 -0
- data/lib/rodauth/rails/tasks.rake +28 -0
- data/lib/rodauth/rails/version.rb +1 -1
- data/rodauth-rails.gemspec +3 -3
- metadata +29 -7
@@ -1,179 +1,5 @@
|
|
1
|
-
class
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
2
|
def change
|
3
|
-
|
4
|
-
enable_extension "citext"
|
5
|
-
|
6
|
-
<% end -%>
|
7
|
-
create_table :accounts do |t|
|
8
|
-
<% case activerecord_adapter -%>
|
9
|
-
<% when "postgresql" -%>
|
10
|
-
t.citext :email, null: false, index: { unique: true, where: "status IN ('verified', 'unverified')" }
|
11
|
-
<% else -%>
|
12
|
-
t.string :email, null: false, index: { unique: true }
|
13
|
-
<% end -%>
|
14
|
-
t.string :status, null: false, default: "verified"
|
15
|
-
end
|
16
|
-
|
17
|
-
# Used if storing password hashes in a separate table (default)
|
18
|
-
create_table :account_password_hashes do |t|
|
19
|
-
t.foreign_key :accounts, column: :id
|
20
|
-
t.string :password_hash, null: false
|
21
|
-
end
|
22
|
-
|
23
|
-
# Used by the password reset feature
|
24
|
-
create_table :account_password_reset_keys do |t|
|
25
|
-
t.foreign_key :accounts, column: :id
|
26
|
-
t.string :key, null: false
|
27
|
-
t.datetime :deadline, null: false
|
28
|
-
t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
29
|
-
end
|
30
|
-
|
31
|
-
# Used by the account verification feature
|
32
|
-
create_table :account_verification_keys do |t|
|
33
|
-
t.foreign_key :accounts, column: :id
|
34
|
-
t.string :key, null: false
|
35
|
-
t.datetime :requested_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
36
|
-
t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
37
|
-
end
|
38
|
-
|
39
|
-
# Used by the verify login change feature
|
40
|
-
create_table :account_login_change_keys do |t|
|
41
|
-
t.foreign_key :accounts, column: :id
|
42
|
-
t.string :key, null: false
|
43
|
-
t.string :login, null: false
|
44
|
-
t.datetime :deadline, null: false
|
45
|
-
end
|
46
|
-
|
47
|
-
<% unless api_only? -%>
|
48
|
-
# Used by the remember me feature
|
49
|
-
create_table :account_remember_keys do |t|
|
50
|
-
t.foreign_key :accounts, column: :id
|
51
|
-
t.string :key, null: false
|
52
|
-
t.datetime :deadline, null: false
|
53
|
-
end
|
54
|
-
<% else -%>
|
55
|
-
# # Used by the remember me feature
|
56
|
-
# create_table :account_remember_keys do |t|
|
57
|
-
# t.foreign_key :accounts, column: :id
|
58
|
-
# t.string :key, null: false
|
59
|
-
# t.datetime :deadline, null: false
|
60
|
-
# end
|
61
|
-
<% end -%>
|
62
|
-
|
63
|
-
# # Used by the audit logging feature
|
64
|
-
# create_table :account_authentication_audit_logs do |t|
|
65
|
-
# t.references :account, foreign_key: true, null: false
|
66
|
-
# t.datetime :at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
67
|
-
# t.text :message, null: false
|
68
|
-
<% case activerecord_adapter -%>
|
69
|
-
<% when "postgresql" -%>
|
70
|
-
# t.jsonb :metadata
|
71
|
-
<% when "sqlite3", "mysql2" -%>
|
72
|
-
# t.json :metadata
|
73
|
-
<% else -%>
|
74
|
-
# t.string :metadata
|
75
|
-
<% end -%>
|
76
|
-
# t.index [:account_id, :at], name: "audit_account_at_idx"
|
77
|
-
# t.index :at, name: "audit_at_idx"
|
78
|
-
# end
|
79
|
-
|
80
|
-
# # Used by the jwt refresh feature
|
81
|
-
# create_table :account_jwt_refresh_keys do |t|
|
82
|
-
# t.references :account, foreign_key: true, null: false
|
83
|
-
# t.string :key, null: false
|
84
|
-
# t.datetime :deadline, null: false
|
85
|
-
# t.index :account_id, name: "account_jwt_rk_account_id_idx"
|
86
|
-
# end
|
87
|
-
|
88
|
-
# # Used by the disallow_password_reuse feature
|
89
|
-
# create_table :account_previous_password_hashes do |t|
|
90
|
-
# t.references :account, foreign_key: true
|
91
|
-
# t.string :password_hash, null: false
|
92
|
-
# end
|
93
|
-
|
94
|
-
# # Used by the lockout feature
|
95
|
-
# create_table :account_login_failures do |t|
|
96
|
-
# t.foreign_key :accounts, column: :id
|
97
|
-
# t.integer :number, null: false, default: 1
|
98
|
-
# end
|
99
|
-
# create_table :account_lockouts do |t|
|
100
|
-
# t.foreign_key :accounts, column: :id
|
101
|
-
# t.string :key, null: false
|
102
|
-
# t.datetime :deadline, null: false
|
103
|
-
# t.datetime :email_last_sent
|
104
|
-
# end
|
105
|
-
|
106
|
-
# # Used by the email auth feature
|
107
|
-
# create_table :account_email_auth_keys do |t|
|
108
|
-
# t.foreign_key :accounts, column: :id
|
109
|
-
# t.string :key, null: false
|
110
|
-
# t.datetime :deadline, null: false
|
111
|
-
# t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
112
|
-
# end
|
113
|
-
|
114
|
-
# # Used by the password expiration feature
|
115
|
-
# create_table :account_password_change_times do |t|
|
116
|
-
# t.foreign_key :accounts, column: :id
|
117
|
-
# t.datetime :changed_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
118
|
-
# end
|
119
|
-
|
120
|
-
# # Used by the account expiration feature
|
121
|
-
# create_table :account_activity_times do |t|
|
122
|
-
# t.foreign_key :accounts, column: :id
|
123
|
-
# t.datetime :last_activity_at, null: false
|
124
|
-
# t.datetime :last_login_at, null: false
|
125
|
-
# t.datetime :expired_at
|
126
|
-
# end
|
127
|
-
|
128
|
-
# # Used by the single session feature
|
129
|
-
# create_table :account_session_keys do |t|
|
130
|
-
# t.foreign_key :accounts, column: :id
|
131
|
-
# t.string :key, null: false
|
132
|
-
# end
|
133
|
-
|
134
|
-
# # Used by the active sessions feature
|
135
|
-
# create_table :account_active_session_keys, primary_key: [:account_id, :session_id] do |t|
|
136
|
-
# t.references :account, foreign_key: true
|
137
|
-
# t.string :session_id
|
138
|
-
# t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
139
|
-
# t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
140
|
-
# end
|
141
|
-
|
142
|
-
# # Used by the webauthn feature
|
143
|
-
# create_table :account_webauthn_user_ids do |t|
|
144
|
-
# t.foreign_key :accounts, column: :id
|
145
|
-
# t.string :webauthn_id, null: false
|
146
|
-
# end
|
147
|
-
# create_table :account_webauthn_keys, primary_key: [:account_id, :webauthn_id] do |t|
|
148
|
-
# t.references :account, foreign_key: true
|
149
|
-
# t.string :webauthn_id
|
150
|
-
# t.string :public_key, null: false
|
151
|
-
# t.integer :sign_count, null: false
|
152
|
-
# t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
153
|
-
# end
|
154
|
-
|
155
|
-
# # Used by the otp feature
|
156
|
-
# create_table :account_otp_keys do |t|
|
157
|
-
# t.foreign_key :accounts, column: :id
|
158
|
-
# t.string :key, null: false
|
159
|
-
# t.integer :num_failures, null: false, default: 0
|
160
|
-
# t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
161
|
-
# end
|
162
|
-
|
163
|
-
# # Used by the recovery codes feature
|
164
|
-
# create_table :account_recovery_codes, primary_key: [:id, :code] do |t|
|
165
|
-
# t.integer :id
|
166
|
-
# t.foreign_key :accounts, column: :id
|
167
|
-
# t.string :code
|
168
|
-
# end
|
169
|
-
|
170
|
-
# # Used by the sms codes feature
|
171
|
-
# create_table :account_sms_codes do |t|
|
172
|
-
# t.foreign_key :accounts, column: :id
|
173
|
-
# t.string :phone_number, null: false
|
174
|
-
# t.integer :num_failures
|
175
|
-
# t.string :code
|
176
|
-
# t.datetime :code_issued_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
177
|
-
# end
|
3
|
+
<%= migration_content -%>
|
178
4
|
end
|
179
5
|
end
|
data/lib/rodauth/rails.rb
CHANGED
@@ -9,14 +9,33 @@ module Rodauth
|
|
9
9
|
# This allows the developer to avoid loading Rodauth at boot time.
|
10
10
|
autoload :App, "rodauth/rails/app"
|
11
11
|
|
12
|
-
def self.configure
|
13
|
-
yield self
|
14
|
-
end
|
15
|
-
|
16
12
|
@app = nil
|
17
13
|
@middleware = true
|
18
14
|
|
19
15
|
class << self
|
16
|
+
def rodauth(name = nil)
|
17
|
+
url_options = ActionMailer::Base.default_url_options
|
18
|
+
|
19
|
+
scheme = url_options[:protocol] || "http"
|
20
|
+
port = url_options[:port]
|
21
|
+
port ||= Rack::Request::DEFAULT_PORTS[scheme] if Gem::Version.new(Rack.release) < Gem::Version.new("2.0")
|
22
|
+
host = url_options[:host]
|
23
|
+
host += ":#{port}" if port
|
24
|
+
|
25
|
+
rack_env = {
|
26
|
+
"HTTP_HOST" => host,
|
27
|
+
"rack.url_scheme" => scheme,
|
28
|
+
}
|
29
|
+
|
30
|
+
scope = app.new(rack_env)
|
31
|
+
|
32
|
+
scope.rodauth(name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def configure
|
36
|
+
yield self
|
37
|
+
end
|
38
|
+
|
20
39
|
attr_writer :app
|
21
40
|
attr_writer :middleware
|
22
41
|
|
data/lib/rodauth/rails/app.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Rodauth
|
2
|
+
module Rails
|
3
|
+
class App
|
4
|
+
# Roda plugin that extends middleware plugin by propagating response headers.
|
5
|
+
module Middleware
|
6
|
+
def self.load_dependencies(app)
|
7
|
+
app.plugin :hooks
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.configure(app)
|
11
|
+
app.after do
|
12
|
+
if response.empty? && response.headers.any?
|
13
|
+
env["rodauth.rails.headers"] = response.headers
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
app.plugin :middleware, handle_result: -> (env, res) do
|
18
|
+
if headers = env.delete("rodauth.rails.headers")
|
19
|
+
res[1] = headers.merge(res[1])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -9,10 +9,11 @@ module Rodauth
|
|
9
9
|
:rails_csrf_param,
|
10
10
|
:rails_csrf_token,
|
11
11
|
:rails_check_csrf!,
|
12
|
-
:rails_controller_instance,
|
13
12
|
:rails_controller,
|
14
13
|
)
|
15
14
|
|
15
|
+
auth_cached_method :rails_controller_instance
|
16
|
+
|
16
17
|
# Renders templates with layout. First tries to render a user-defined
|
17
18
|
# template, otherwise falls back to Rodauth's template.
|
18
19
|
def view(page, *)
|
@@ -28,6 +29,11 @@ module Rodauth
|
|
28
29
|
super
|
29
30
|
end
|
30
31
|
|
32
|
+
# Render Rails CSRF tags in Rodauth templates.
|
33
|
+
def csrf_tag(*)
|
34
|
+
rails_csrf_tag
|
35
|
+
end
|
36
|
+
|
31
37
|
# Verify Rails' authenticity token.
|
32
38
|
def check_csrf
|
33
39
|
rails_check_csrf!
|
@@ -38,11 +44,6 @@ module Rodauth
|
|
38
44
|
true
|
39
45
|
end
|
40
46
|
|
41
|
-
# Render Rails CSRF tags in Rodauth templates.
|
42
|
-
def csrf_tag(*)
|
43
|
-
rails_csrf_tag
|
44
|
-
end
|
45
|
-
|
46
47
|
# Default the flash error key to Rails' default :alert.
|
47
48
|
def flash_error_key
|
48
49
|
:alert
|
@@ -50,6 +51,59 @@ module Rodauth
|
|
50
51
|
|
51
52
|
private
|
52
53
|
|
54
|
+
# Runs controller callbacks and rescue handlers around Rodauth actions.
|
55
|
+
def _around_rodauth(&block)
|
56
|
+
result = nil
|
57
|
+
|
58
|
+
rails_controller_rescue do
|
59
|
+
rails_controller_callbacks do
|
60
|
+
result = catch(:halt) { super(&block) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if rails_controller_instance.performed?
|
65
|
+
rails_controller_response
|
66
|
+
else
|
67
|
+
result[1].merge!(rails_controller_instance.response.headers)
|
68
|
+
throw :halt, result
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Runs any #(before|around|after)_action controller callbacks.
|
73
|
+
def rails_controller_callbacks
|
74
|
+
# don't verify CSRF token as part of callbacks, Rodauth will do that
|
75
|
+
rails_controller_instance.allow_forgery_protection = false
|
76
|
+
|
77
|
+
rails_controller_instance.run_callbacks(:process_action) do
|
78
|
+
# turn the setting back to default so that form tags generate CSRF tags
|
79
|
+
rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
|
80
|
+
|
81
|
+
yield
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Runs any registered #rescue_from controller handlers.
|
86
|
+
def rails_controller_rescue
|
87
|
+
yield
|
88
|
+
rescue Exception => exception
|
89
|
+
rails_controller_instance.rescue_with_handler(exception) || raise
|
90
|
+
|
91
|
+
unless rails_controller_instance.performed?
|
92
|
+
raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns Roda response from controller response if set.
|
97
|
+
def rails_controller_response
|
98
|
+
controller_response = rails_controller_instance.response
|
99
|
+
|
100
|
+
response.status = controller_response.status
|
101
|
+
response.headers.merge! controller_response.headers
|
102
|
+
response.write controller_response.body
|
103
|
+
|
104
|
+
request.halt
|
105
|
+
end
|
106
|
+
|
53
107
|
# Create emails with ActionMailer which uses configured delivery method.
|
54
108
|
def create_email_to(to, subject, body)
|
55
109
|
Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
|
@@ -62,11 +116,18 @@ module Rodauth
|
|
62
116
|
|
63
117
|
# Calls the Rails renderer, returning nil if a template is missing.
|
64
118
|
def rails_render(*args)
|
119
|
+
return if only_json?
|
120
|
+
|
65
121
|
rails_controller_instance.render_to_string(*args)
|
66
122
|
rescue ActionView::MissingTemplate
|
67
123
|
nil
|
68
124
|
end
|
69
125
|
|
126
|
+
# Calls the controller to verify the authenticity token.
|
127
|
+
def rails_check_csrf!
|
128
|
+
rails_controller_instance.send(:verify_authenticity_token)
|
129
|
+
end
|
130
|
+
|
70
131
|
# Hidden tag with Rails CSRF token inserted into Rodauth templates.
|
71
132
|
def rails_csrf_tag
|
72
133
|
%(<input type="hidden" name="#{rails_csrf_param}" value="#{rails_csrf_token}">)
|
@@ -82,30 +143,36 @@ module Rodauth
|
|
82
143
|
rails_controller_instance.send(:form_authenticity_token)
|
83
144
|
end
|
84
145
|
|
85
|
-
# Calls the controller to verify the authenticity token.
|
86
|
-
def rails_check_csrf!
|
87
|
-
rails_controller_instance.send(:verify_authenticity_token)
|
88
|
-
end
|
89
|
-
|
90
146
|
# Instances of the configured controller with current request's env hash.
|
91
|
-
def
|
92
|
-
|
93
|
-
|
147
|
+
def _rails_controller_instance
|
148
|
+
controller = rails_controller.new
|
149
|
+
rails_request = ActionDispatch::Request.new(scope.env)
|
94
150
|
|
95
|
-
|
96
|
-
instance.set_request! request
|
97
|
-
instance.set_response! rails_controller.make_response!(request)
|
98
|
-
else
|
99
|
-
instance.send(:set_response!, request)
|
100
|
-
instance.instance_variable_set(:@_request, request)
|
101
|
-
end
|
151
|
+
prepare_rails_controller(controller, rails_request)
|
102
152
|
|
103
|
-
|
153
|
+
controller
|
104
154
|
end
|
105
155
|
|
106
|
-
|
107
|
-
|
108
|
-
|
156
|
+
if ActionPack.version >= Gem::Version.new("5.0")
|
157
|
+
# Controller class to use for view rendering, CSRF protection, and
|
158
|
+
# running any registered action callbacks and rescue_from handlers.
|
159
|
+
def rails_controller
|
160
|
+
only_json? ? ActionController::API : ActionController::Base
|
161
|
+
end
|
162
|
+
|
163
|
+
def prepare_rails_controller(controller, rails_request)
|
164
|
+
controller.set_request! rails_request
|
165
|
+
controller.set_response! rails_controller.make_response!(rails_request)
|
166
|
+
end
|
167
|
+
else
|
168
|
+
def rails_controller
|
169
|
+
ActionController::Base
|
170
|
+
end
|
171
|
+
|
172
|
+
def prepare_rails_controller(controller, rails_request)
|
173
|
+
controller.send(:set_response!, rails_request)
|
174
|
+
controller.instance_variable_set(:@_request, rails_request)
|
175
|
+
end
|
109
176
|
end
|
110
177
|
|
111
178
|
# ActionMailer subclass for correct email delivering.
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require "rodauth/rails/middleware"
|
2
2
|
require "rodauth/rails/controller_methods"
|
3
3
|
|
4
|
+
require "rails"
|
5
|
+
|
4
6
|
module Rodauth
|
5
7
|
module Rails
|
6
8
|
class Railtie < ::Rails::Railtie
|
@@ -13,6 +15,15 @@ module Rodauth
|
|
13
15
|
include Rodauth::Rails::ControllerMethods
|
14
16
|
end
|
15
17
|
end
|
18
|
+
|
19
|
+
initializer "rodauth.test" do
|
20
|
+
# Rodauth uses RACK_ENV to set the default bcrypt hash cost
|
21
|
+
ENV["RACK_ENV"] = "test" if ::Rails.env.test?
|
22
|
+
end
|
23
|
+
|
24
|
+
rake_tasks do
|
25
|
+
load "rodauth/rails/tasks.rake"
|
26
|
+
end
|
16
27
|
end
|
17
28
|
end
|
18
29
|
end
|