rodauth-rails 0.9.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +417 -250
  4. data/lib/generators/rodauth/install_generator.rb +17 -0
  5. data/lib/generators/rodauth/migration/base.erb +2 -2
  6. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +31 -29
  7. data/lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb +3 -3
  8. data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +1 -1
  9. data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +2 -2
  10. data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +2 -2
  11. data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +2 -2
  12. data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +2 -2
  13. data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +2 -2
  14. data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +2 -2
  15. data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +2 -2
  16. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +2 -2
  17. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +2 -2
  18. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +2 -2
  19. data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
  20. data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +2 -2
  21. data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +1 -1
  22. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +1 -1
  23. data/lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb +1 -1
  24. data/lib/rodauth/rails.rb +32 -4
  25. data/lib/rodauth/rails/app.rb +20 -22
  26. data/lib/rodauth/rails/app/flash.rb +2 -8
  27. data/lib/rodauth/rails/app/middleware.rb +20 -10
  28. data/lib/rodauth/rails/auth.rb +40 -0
  29. data/lib/rodauth/rails/controller_methods.rb +1 -5
  30. data/lib/rodauth/rails/feature.rb +17 -210
  31. data/lib/rodauth/rails/feature/base.rb +62 -0
  32. data/lib/rodauth/rails/feature/callbacks.rb +61 -0
  33. data/lib/rodauth/rails/feature/csrf.rb +65 -0
  34. data/lib/rodauth/rails/feature/email.rb +30 -0
  35. data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
  36. data/lib/rodauth/rails/feature/render.rb +41 -0
  37. data/lib/rodauth/rails/version.rb +1 -1
  38. data/rodauth-rails.gemspec +1 -1
  39. metadata +12 -6
  40. data/lib/generators/rodauth/mailer_generator.rb +0 -37
@@ -10,6 +10,15 @@ module Rodauth
10
10
  include ::ActiveRecord::Generators::Migration
11
11
  include MigrationHelpers
12
12
 
13
+ MAILER_VIEWS = %w[
14
+ email_auth
15
+ password_changed
16
+ reset_password
17
+ unlock_account
18
+ verify_account
19
+ verify_login_change
20
+ ]
21
+
13
22
  source_root "#{__dir__}/templates"
14
23
  namespace "rodauth:install"
15
24
 
@@ -47,6 +56,14 @@ module Rodauth
47
56
  template "app/models/account.rb"
48
57
  end
49
58
 
59
+ def create_mailer
60
+ template "app/mailers/rodauth_mailer.rb"
61
+
62
+ MAILER_VIEWS.each do |view|
63
+ template "app/views/rodauth_mailer/#{view}.text.erb"
64
+ end
65
+ end
66
+
50
67
  private
51
68
 
52
69
  def sequel_uri_scheme
@@ -5,11 +5,11 @@ enable_extension "citext"
5
5
  create_table :accounts<%= primary_key_type %> do |t|
6
6
  <% case activerecord_adapter -%>
7
7
  <% when "postgresql" -%>
8
- t.citext :email, null: false, index: { unique: true, where: "status IN ('verified', 'unverified')" }
8
+ t.citext :email, null: false, index: { unique: true, where: "status IN ('unverified', 'verified')" }
9
9
  <% else -%>
10
10
  t.string :email, null: false, index: { unique: true }
11
11
  <% end -%>
12
- t.string :status, null: false, default: "verified"
12
+ t.string :status, null: false, default: "unverified"
13
13
  end
14
14
 
15
15
  # Used if storing password hashes in a separate table (default)
@@ -2,10 +2,10 @@ class RodauthApp < Rodauth::Rails::App
2
2
  configure do
3
3
  # List of authentication features that are loaded.
4
4
  enable :create_account, :verify_account, :verify_account_grace_period,
5
- :login, :logout<%= ", :remember" unless jwt? %>,
5
+ :login, :logout<%= ", :remember" unless jwt? %><%= ", :json" if json? %><%= ", :jwt" if jwt? %>,
6
6
  :reset_password, :change_password, :change_password_notify,
7
7
  :change_login, :verify_login_change,
8
- :close_account<%= ", :json" if json? %><%= ", :jwt" if jwt? %>
8
+ :close_account
9
9
 
10
10
  # See the Rodauth documentation for the list of available config options:
11
11
  # http://rodauth.jeremyevans.net/documentation.html
@@ -23,6 +23,10 @@ class RodauthApp < Rodauth::Rails::App
23
23
 
24
24
  # Accept only JSON requests.
25
25
  only_json? true
26
+
27
+ # Handle login and password confirmation fields on the client side.
28
+ # require_password_confirmation? false
29
+ # require_login_confirmation? false
26
30
  <% end -%>
27
31
 
28
32
  # Specify the controller used for view rendering and CSRF verification.
@@ -54,35 +58,29 @@ class RodauthApp < Rodauth::Rails::App
54
58
  # already_logged_in { redirect login_redirect }
55
59
 
56
60
  # ==> Emails
57
- # Uncomment the lines below once you've imported mailer views.
58
- # create_reset_password_email do
59
- # RodauthMailer.reset_password(email_to, reset_password_email_link)
60
- # end
61
- # create_verify_account_email do
62
- # RodauthMailer.verify_account(email_to, verify_account_email_link)
63
- # end
64
- # create_verify_login_change_email do |login|
65
- # RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
66
- # end
67
- # create_password_changed_email do
68
- # RodauthMailer.password_changed(email_to)
61
+ # Use a custom mailer for delivering authentication emails.
62
+ create_reset_password_email do
63
+ RodauthMailer.reset_password(email_to, reset_password_email_link)
64
+ end
65
+ create_verify_account_email do
66
+ RodauthMailer.verify_account(email_to, verify_account_email_link)
67
+ end
68
+ create_verify_login_change_email do |login|
69
+ RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
70
+ end
71
+ create_password_changed_email do
72
+ RodauthMailer.password_changed(email_to)
73
+ end
74
+ # create_email_auth_email do
75
+ # RodauthMailer.email_auth(email_to, email_auth_email_link)
69
76
  # end
70
- # # create_email_auth_email do
71
- # # RodauthMailer.email_auth(email_to, email_auth_email_link)
72
- # # end
73
- # # create_unlock_account_email do
74
- # # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
75
- # # end
76
- # send_email do |email|
77
- # # queue email delivery on the mailer after the transaction commits
78
- # db.after_commit { email.deliver_later }
77
+ # create_unlock_account_email do
78
+ # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
79
79
  # end
80
-
81
- # In the meantime, you can tweak settings for emails created by Rodauth.
82
- # email_subject_prefix "[MyApp] "
83
- # email_from "noreply@myapp.com"
84
- # send_email(&:deliver_later)
85
- # reset_password_email_body { "Click here to reset your password: #{reset_password_email_link}" }
80
+ send_email do |email|
81
+ # queue email delivery on the mailer after the transaction commits
82
+ db.after_commit { email.deliver_later }
83
+ end
86
84
 
87
85
  # ==> Flash
88
86
  <% unless json? || jwt? -%>
@@ -151,7 +149,9 @@ class RodauthApp < Rodauth::Rails::App
151
149
  # verify_account_grace_period 3.days
152
150
  # reset_password_deadline_interval Hash[hours: 6]
153
151
  # verify_login_change_deadline_interval Hash[days: 2]
152
+ <% unless jwt? -%>
154
153
  # remember_deadline_interval Hash[days: 30]
154
+ <% end -%>
155
155
  end
156
156
 
157
157
  # ==> Multiple configurations
@@ -187,6 +187,8 @@ class RodauthApp < Rodauth::Rails::App
187
187
  # unless rodauth(:admin).logged_in?
188
188
  # rodauth(:admin).require_http_basic_auth
189
189
  # end
190
+ #
191
+ # r.pass # allow the Rails app to handle other "/admin/*" requests
190
192
  # end
191
193
  end
192
194
  end
@@ -1,4 +1,4 @@
1
- class <%= options[:name].camelize %>Mailer < ApplicationMailer
1
+ class RodauthMailer < ApplicationMailer
2
2
  def verify_account(recipient, email_link)
3
3
  @email_link = email_link
4
4
 
@@ -25,13 +25,13 @@ class <%= options[:name].camelize %>Mailer < ApplicationMailer
25
25
 
26
26
  # def email_auth(recipient, email_link)
27
27
  # @email_link = email_link
28
-
28
+ #
29
29
  # mail to: recipient
30
30
  # end
31
31
 
32
32
  # def unlock_account(recipient, email_link)
33
33
  # @email_link = email_link
34
-
34
+ #
35
35
  # mail to: recipient
36
36
  # end
37
37
  end
@@ -1,4 +1,4 @@
1
- <div class="form-group">
1
+ <div class="form-group mb-3">
2
2
  <div class="form-check">
3
3
  <%%= check_box_tag rodauth.global_logout_param, "t", false, id: "global-logout", class: "form-check-input" %>
4
4
  <%%= label_tag "global-logout", "Logout all Logged In Sessons?", class: "form-check-label" %>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "login-confirm", "Confirm Login" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "login-confirm", "Confirm Login", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.login_confirm_param, id: "login-confirm", type: :email, autocomplete: "email" %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "login", "Login" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "login", "Login", class: "form-label" %>
3
3
  <%%= email_field_tag rodauth.login_param, params[rodauth.login_param], id: "login", readonly: true, class: "form-control-plaintext" %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "login", "Login" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "login", "Login", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.login_param, id: "login", type: :email, autocomplete: "email" %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "new-password", "New Password" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "new-password", "New Password", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.new_password_param, id: "new-password", type: "password", value: "", autocomplete: "new-password" %>
4
4
  </div>
@@ -1,5 +1,5 @@
1
- <div class="form-group">
2
- <%%= label_tag "otp-auth-code", "Authentication Code" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "otp-auth-code", "Authentication Code", class: "form-label" %>
3
3
  <div class="row">
4
4
  <div class="col-sm-3">
5
5
  <%%= render "field", name: rodauth.otp_auth_param, id: "otp-auth-code", value: "", autocomplete: "off", inputmode: "numeric" %>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "password-confirm", "Confirm Password" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "password-confirm", "Confirm Password", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.password_confirm_param, id: "password-confirm", type: :password, value: "", autocomplete: "new-password" %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "password", "Password" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "password", "Password", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.password_param, id: "password", type: :password, value: "", autocomplete: rodauth.password_field_autocomplete_value %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "recovery_code", "Recovery Code" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "recovery_code", "Recovery Code", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.recovery_codes_param, id: "recovery_code", value: "", autocomplete: "off" %>
4
4
  </div>
@@ -1,5 +1,5 @@
1
- <div class="form-group">
2
- <%%= label_tag "sms-code", "SMS Code" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "sms-code", "SMS Code", class: "form-label" %>
3
3
  <div class="row">
4
4
  <div class="col-sm-3">
5
5
  <%%= render "field", name: rodauth.sms_code_param, id: "sms-code", value: "", autocomplete: "one-time-code", inputmode: "numeric" %>
@@ -1,5 +1,5 @@
1
- <div class="form-group">
2
- <%%= label_tag "sms-phone", "Phone Number" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "sms-phone", "Phone Number", class: "form-label" %>
3
3
  <div class="row">
4
4
  <div class="col-sm-3">
5
5
  <%%= render "field", name: rodauth.sms_phone_param, id: "sms-phone", type: :tel, autocomplete: "tel" %>
@@ -1,3 +1,3 @@
1
- <div class="form-group">
1
+ <div class="form-group mb-3">
2
2
  <%%= submit_tag local_assigns[:value], name: local_assigns[:name], class: local_assigns[:class] || "btn btn-primary" %>
3
3
  </div>
@@ -2,14 +2,14 @@
2
2
  <%%= hidden_field_tag rodauth.otp_setup_param, rodauth.otp_user_key, id: "otp-key" %>
3
3
  <%%= hidden_field_tag rodauth.otp_setup_raw_param, rodauth.otp_key, id: "otp-hmac-secret" if rodauth.otp_keys_use_hmac? %>
4
4
 
5
- <div class="form-group">
5
+ <div class="form-group mb-3">
6
6
  <p>Secret: <%%= rodauth.otp_user_key %></p>
7
7
  <p>Provisioning URL: <%%= rodauth.otp_provisioning_uri %></p>
8
8
  </div>
9
9
 
10
10
  <div class="row">
11
11
  <div class="col-lg-6 col-lg">
12
- <div class="form-group">
12
+ <div class="form-group mb-3">
13
13
  <p><%%= rodauth.otp_qr_code.html_safe %></p>
14
14
  </div>
15
15
  </div>
@@ -1,5 +1,5 @@
1
1
  <%%= form_tag rodauth.remember_path, method: :post do %>
2
- <fieldset class="form-group">
2
+ <fieldset class="form-group mb-3">
3
3
  <div class="form-check">
4
4
  <%%= radio_button_tag rodauth.remember_param, rodauth.remember_remember_param_value, false, id: "remember-remember", class: "form-check-input" %>
5
5
  <%%= label_tag "remember-remember", "Remember Me", class: "form-check-label" %>
@@ -1,6 +1,6 @@
1
1
  <%%= form_tag rodauth.webauthn_remove_path, method: :post, id: "webauthn-remove-form" do %>
2
2
  <%%= render "password_field" if rodauth.two_factor_modifications_require_password? %>
3
- <fieldset class="form-group">
3
+ <fieldset class="form-group mb-3">
4
4
  <%% (usage = rodauth.account_webauthn_usage).each do |id, last_use| %>
5
5
  <div class="form-check">
6
6
  <%%= render "field", name: rodauth.webauthn_remove_param, id: "webauthn-remove-#{id}", type: :radio, class: "form-check-input", skip_error_message: true, value: id, required: false %>
@@ -1,4 +1,4 @@
1
- Someone has requested a that the account with this email be unlocked.
1
+ Someone has requested that the account with this email be unlocked.
2
2
  If you did not request the unlocking of this account, please ignore this
3
3
  message. If you requested the unlocking of this account, please go to
4
4
  <%%= @email_link %>
data/lib/rodauth/rails.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  require "rodauth/rails/version"
2
2
  require "rodauth/rails/railtie"
3
3
 
4
+ require "rack/utils"
5
+ require "stringio"
6
+
4
7
  module Rodauth
5
8
  module Rails
6
9
  class Error < StandardError
@@ -8,12 +11,13 @@ module Rodauth
8
11
 
9
12
  # This allows the developer to avoid loading Rodauth at boot time.
10
13
  autoload :App, "rodauth/rails/app"
14
+ autoload :Auth, "rodauth/rails/auth"
11
15
 
12
16
  @app = nil
13
17
  @middleware = true
14
18
 
15
19
  class << self
16
- def rodauth(name = nil)
20
+ def rodauth(name = nil, query: {}, form: {}, session: {}, account: nil, env: {})
17
21
  url_options = ActionMailer::Base.default_url_options
18
22
 
19
23
  scheme = url_options[:protocol] || "http"
@@ -22,14 +26,38 @@ module Rodauth
22
26
  host = url_options[:host]
23
27
  host += ":#{port}" if port
24
28
 
29
+ content_type = "application/x-www-form-urlencoded" if form.any?
30
+
25
31
  rack_env = {
32
+ "QUERY_STRING" => Rack::Utils.build_nested_query(query),
33
+ "rack.input" => StringIO.new(Rack::Utils.build_nested_query(form)),
34
+ "CONTENT_TYPE" => content_type,
35
+ "rack.session" => {},
26
36
  "HTTP_HOST" => host,
27
37
  "rack.url_scheme" => scheme,
28
- }
38
+ }.merge(env)
39
+
40
+ scope = app.new(rack_env)
41
+ instance = scope.rodauth(name)
42
+
43
+ # update session hash here to make it work with JWT session
44
+ instance.session.merge!(session)
29
45
 
30
- scope = app.new(rack_env)
46
+ if account
47
+ instance.instance_variable_set(:@account, account.attributes.symbolize_keys)
48
+ instance.session[instance.session_key] = instance.account_session_value
49
+ end
50
+
51
+ instance
52
+ end
31
53
 
32
- scope.rodauth(name)
54
+ # routing constraint that requires authentication
55
+ def authenticated(name = nil, &condition)
56
+ lambda do |request|
57
+ rodauth = request.env.fetch ["rodauth", *name].join(".")
58
+ rodauth.require_authentication
59
+ rodauth.authenticated? && (condition.nil? || condition.call(rodauth))
60
+ end
33
61
  end
34
62
 
35
63
  if ::Rails.gem_version >= Gem::Version.new("5.2")
@@ -1,6 +1,5 @@
1
1
  require "roda"
2
- require "rodauth"
3
- require "rodauth/rails/feature"
2
+ require "rodauth/rails/auth"
4
3
 
5
4
  module Rodauth
6
5
  module Rails
@@ -11,40 +10,39 @@ module Rodauth
11
10
 
12
11
  plugin :hooks
13
12
  plugin :render, layout: false
13
+ plugin :pass
14
14
 
15
- if defined?(ActionDispatch::Flash) # not in API-only mode
15
+ unless Rodauth::Rails.api_only?
16
16
  require "rodauth/rails/app/flash"
17
17
  plugin Flash
18
18
  end
19
19
 
20
- def self.configure(name = nil, **options, &block)
21
- plugin :rodauth, name: name, csrf: false, flash: false, json: true, **options do
22
- # load the Rails integration
23
- enable :rails
20
+ def self.configure(*args, **options, &block)
21
+ auth_class = args.shift if args[0].is_a?(Class)
22
+ name = args.shift if args[0].is_a?(Symbol)
24
23
 
25
- # database functions are more complex to set up, so disable them by default
26
- use_database_authentication_functions? false
24
+ fail ArgumentError, "need to pass optional Rodauth::Auth subclass and optional configuration name" if args.any?
27
25
 
28
- # avoid having to set deadline values in column default values
29
- set_deadline_values? true
26
+ auth_class ||= Class.new(Rodauth::Rails::Auth)
30
27
 
31
- # use HMACs for additional security
32
- hmac_secret { Rodauth::Rails.secret_key_base }
33
-
34
- # evaluate user configuration
35
- instance_exec(&block)
28
+ plugin :rodauth, auth_class: auth_class, name: name, csrf: false, flash: false, json: true, **options do
29
+ instance_exec(&block) if block
36
30
  end
37
31
  end
38
32
 
39
33
  before do
40
- (opts[:rodauths] || {}).each do |name, _|
41
- if name
42
- env["rodauth.#{name}"] = rodauth(name)
43
- else
44
- env["rodauth"] = rodauth
45
- end
34
+ opts[:rodauths]&.each_key do |name|
35
+ env[["rodauth", *name].join(".")] = rodauth(name)
46
36
  end
47
37
  end
38
+
39
+ def rails_routes
40
+ ::Rails.application.routes.url_helpers
41
+ end
42
+
43
+ def rails_request
44
+ ActionDispatch::Request.new(env)
45
+ end
48
46
  end
49
47
  end
50
48
  end
@@ -27,24 +27,18 @@ module Rodauth
27
27
  end
28
28
 
29
29
  def flash
30
- rails_request.flash
30
+ scope.rails_request.flash
31
31
  end
32
32
 
33
33
  if ActionPack.version >= Gem::Version.new("5.0")
34
34
  def commit_flash
35
- rails_request.commit_flash
35
+ scope.rails_request.commit_flash
36
36
  end
37
37
  else
38
38
  def commit_flash
39
39
  # ActionPack 4.2 automatically commits flash
40
40
  end
41
41
  end
42
-
43
- private
44
-
45
- def rails_request
46
- ActionDispatch::Request.new(env)
47
- end
48
42
  end
49
43
  end
50
44
  end