rodauth-rails 0.8.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,17 +10,20 @@ 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
 
16
- # The :api option is a Rails-recognized option that always
17
- # defaults to false, so we make it use our provided default
18
- # value instead.
19
- def self.default_value_for_option(name, options)
20
- name == :api ? options[:default] : super
21
- end
22
-
23
- class_option :api, type: :boolean, desc: "Generate JSON-only configuration"
25
+ class_option :json, type: :boolean, desc: "Configure JSON support"
26
+ class_option :jwt, type: :boolean, desc: "Configure JWT support"
24
27
 
25
28
  def create_rodauth_migration
26
29
  return unless defined?(ActiveRecord::Base)
@@ -53,6 +56,14 @@ module Rodauth
53
56
  template "app/models/account.rb"
54
57
  end
55
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
+
56
67
  private
57
68
 
58
69
  def sequel_uri_scheme
@@ -83,17 +94,17 @@ module Rodauth
83
94
  end
84
95
  end
85
96
 
86
- def api_only?
87
- if options.key?(:api)
88
- options[:api]
89
- elsif ::Rails.gem_version >= Gem::Version.new("5.0")
90
- ::Rails.application.config.api_only
91
- end
97
+ def json?
98
+ options[:json]
99
+ end
100
+
101
+ def jwt?
102
+ options[:jwt] || Rodauth::Rails.api_only?
92
103
  end
93
104
 
94
105
  def migration_features
95
106
  features = [:base, :reset_password, :verify_account, :verify_login_change]
96
- features << :remember unless api_only?
107
+ features << :remember unless jwt?
97
108
  features
98
109
  end
99
110
  end
@@ -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)
@@ -1,8 +1,8 @@
1
1
  class RodauthApp < Rodauth::Rails::App
2
- configure<%= " json: :only" if api_only? %> do
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, <%= api_only? ? ":jwt" : ":remember" %>,
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
8
  :close_account
@@ -14,6 +14,20 @@ class RodauthApp < Rodauth::Rails::App
14
14
  # The secret key used for hashing public-facing tokens for various features.
15
15
  # Defaults to Rails `secret_key_base`, but you can use your own secret key.
16
16
  # hmac_secret "<%= SecureRandom.hex(64) %>"
17
+ <% if jwt? -%>
18
+
19
+ # Set JWT secret, which is used to cryptographically protect the token.
20
+ jwt_secret "<%= SecureRandom.hex(64) %>"
21
+ <% end -%>
22
+ <% if json? || jwt? -%>
23
+
24
+ # Accept only JSON requests.
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
30
+ <% end -%>
17
31
 
18
32
  # Specify the controller used for view rendering and CSRF verification.
19
33
  rails_controller { RodauthController }
@@ -42,52 +56,34 @@ class RodauthApp < Rodauth::Rails::App
42
56
 
43
57
  # Redirect to the app from login and registration pages if already logged in.
44
58
  # already_logged_in { redirect login_redirect }
45
- <% if api_only? -%>
46
-
47
- # ==> JWT
48
- # Set JWT secret, which is used to cryptographically protect the token.
49
- jwt_secret "<%= SecureRandom.hex(64) %>"
50
-
51
- # Don't require login confirmation param.
52
- require_login_confirmation? false
53
-
54
- # Don't require password confirmation param.
55
- require_password_confirmation? false
56
- <% end -%>
57
59
 
58
60
  # ==> Emails
59
- # Uncomment the lines below once you've imported mailer views.
60
- # create_reset_password_email do
61
- # RodauthMailer.reset_password(email_to, reset_password_email_link)
62
- # end
63
- # create_verify_account_email do
64
- # RodauthMailer.verify_account(email_to, verify_account_email_link)
65
- # end
66
- # create_verify_login_change_email do |login|
67
- # RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
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)
68
76
  # end
69
- # create_password_changed_email do
70
- # RodauthMailer.password_changed(email_to)
77
+ # create_unlock_account_email do
78
+ # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
71
79
  # end
72
- # # create_email_auth_email do
73
- # # RodauthMailer.email_auth(email_to, email_auth_email_link)
74
- # # end
75
- # # create_unlock_account_email do
76
- # # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
77
- # # end
78
- # send_email do |email|
79
- # # queue email delivery on the mailer after the transaction commits
80
- # db.after_commit { email.deliver_later }
81
- # end
82
-
83
- # In the meantime you can tweak settings for emails created by Rodauth
84
- # email_subject_prefix "[MyApp] "
85
- # email_from "noreply@myapp.com"
86
- # send_email(&:deliver_later)
87
- # 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
88
84
 
89
85
  # ==> Flash
90
- <% unless api_only? -%>
86
+ <% unless json? || jwt? -%>
91
87
  # Match flash keys with ones already used in the Rails app.
92
88
  # flash_notice_key :success # default is :notice
93
89
  # flash_error_key :error # default is :alert
@@ -107,7 +103,7 @@ class RodauthApp < Rodauth::Rails::App
107
103
 
108
104
  # Change minimum number of password characters required when creating an account.
109
105
  # password_minimum_length 8
110
- <% unless api_only? -%>
106
+ <% unless jwt? -%>
111
107
 
112
108
  # ==> Remember Feature
113
109
  # Remember all logged in users.
@@ -128,13 +124,14 @@ class RodauthApp < Rodauth::Rails::App
128
124
 
129
125
  # Perform additional actions after the account is created.
130
126
  # after_create_account do
131
- # Profile.create!(account_id: account[:id], name: param("name"))
127
+ # Profile.create!(account_id: account_id, name: param("name"))
132
128
  # end
133
129
 
134
130
  # Do additional cleanup after the account is closed.
135
131
  # after_close_account do
136
- # Profile.find_by!(account_id: account[:id]).destroy
132
+ # Profile.find_by!(account_id: account_id).destroy
137
133
  # end
134
+ <% unless json? || jwt? -%>
138
135
 
139
136
  # ==> Redirects
140
137
  # Redirect to home page after logout.
@@ -145,25 +142,27 @@ class RodauthApp < Rodauth::Rails::App
145
142
 
146
143
  # Redirect to login page after password reset.
147
144
  reset_password_redirect { login_path }
145
+ <% end -%>
148
146
 
149
147
  # ==> Deadlines
150
148
  # Change default deadlines for some actions.
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
158
158
  # configure(:admin) do
159
- # enable :http_basic_auth
160
- #
159
+ # enable :http_basic_auth # enable different set of features
161
160
  # prefix "/admin"
162
- # session_key :admin_id
161
+ # session_key_prefix "admin_"
163
162
  # end
164
163
 
165
164
  route do |r|
166
- <% unless api_only? -%>
165
+ <% unless jwt? -%>
167
166
  rodauth.load_memory # autologin remembered users
168
167
 
169
168
  <% end -%>
@@ -188,6 +187,8 @@ class RodauthApp < Rodauth::Rails::App
188
187
  # unless rodauth(:admin).logged_in?
189
188
  # rodauth(:admin).require_http_basic_auth
190
189
  # end
190
+ #
191
+ # r.pass # allow the Rails app to handle other "/admin/*" requests
191
192
  # end
192
193
  end
193
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
data/lib/rodauth/rails.rb CHANGED
@@ -8,6 +8,7 @@ module Rodauth
8
8
 
9
9
  # This allows the developer to avoid loading Rodauth at boot time.
10
10
  autoload :App, "rodauth/rails/app"
11
+ autoload :Auth, "rodauth/rails/auth"
11
12
 
12
13
  @app = nil
13
14
  @middleware = true
@@ -32,6 +33,15 @@ module Rodauth
32
33
  scope.rodauth(name)
33
34
  end
34
35
 
36
+ # routing constraint that requires authentication
37
+ def authenticated(name = nil, &condition)
38
+ lambda do |request|
39
+ rodauth = request.env.fetch ["rodauth", *name].join(".")
40
+ rodauth.require_authentication
41
+ rodauth.authenticated? && (condition.nil? || condition.call(rodauth))
42
+ end
43
+ end
44
+
35
45
  if ::Rails.gem_version >= Gem::Version.new("5.2")
36
46
  def secret_key_base
37
47
  ::Rails.application.secret_key_base
@@ -42,6 +52,16 @@ module Rodauth
42
52
  end
43
53
  end
44
54
 
55
+ if ::Rails.gem_version >= Gem::Version.new("5.0")
56
+ def api_only?
57
+ ::Rails.application.config.api_only
58
+ end
59
+ else
60
+ def api_only?
61
+ false
62
+ end
63
+ end
64
+
45
65
  def configure
46
66
  yield self
47
67
  end
@@ -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,38 +10,29 @@ module Rodauth
11
10
 
12
11
  plugin :hooks
13
12
  plugin :render, layout: false
13
+ plugin :pass
14
14
 
15
- def self.configure(name = nil, **options, &block)
16
- unless options[:json] == :only
17
- require "rodauth/rails/app/flash"
18
- plugin Flash
19
- end
20
-
21
- plugin :rodauth, name: name, csrf: false, flash: false, **options do
22
- # load the Rails integration
23
- enable :rails
15
+ unless Rodauth::Rails.api_only?
16
+ require "rodauth/rails/app/flash"
17
+ plugin Flash
18
+ end
24
19
 
25
- # database functions are more complex to set up, so disable them by default
26
- use_database_authentication_functions? false
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)
27
23
 
28
- # avoid having to set deadline values in column default values
29
- set_deadline_values? true
24
+ fail ArgumentError, "need to pass optional Rodauth::Auth subclass and optional configuration name" if args.any?
30
25
 
31
- # use HMACs for additional security
32
- hmac_secret { Rodauth::Rails.secret_key_base }
26
+ auth_class ||= Class.new(Rodauth::Rails::Auth)
33
27
 
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
48
38
  end
@@ -30,10 +30,12 @@ module Rodauth
30
30
  rails_request.flash
31
31
  end
32
32
 
33
- def commit_flash
34
- if ActionPack.version >= Gem::Version.new("5.0")
33
+ if ActionPack.version >= Gem::Version.new("5.0")
34
+ def commit_flash
35
35
  rails_request.commit_flash
36
- else
36
+ end
37
+ else
38
+ def commit_flash
37
39
  # ActionPack 4.2 automatically commits flash
38
40
  end
39
41
  end
@@ -3,20 +3,30 @@ module Rodauth
3
3
  class App
4
4
  # Roda plugin that extends middleware plugin by propagating response headers.
5
5
  module Middleware
6
- def self.load_dependencies(app)
7
- app.plugin :hooks
8
- end
9
-
10
6
  def self.configure(app)
11
- app.after do
12
- if response.empty? && response.headers.any?
13
- env["rodauth.rails.headers"] = response.headers
7
+ handle_result = -> (env, res) do
8
+ if headers = env.delete("rodauth.rails.headers")
9
+ res[1] = headers.merge(res[1])
14
10
  end
15
11
  end
16
12
 
17
- app.plugin :middleware, handle_result: -> (env, res) do
18
- if headers = env.delete("rodauth.rails.headers")
19
- res[1] = headers.merge(res[1])
13
+ app.plugin :middleware, handle_result: handle_result do |middleware|
14
+ middleware.plugin :hooks
15
+
16
+ middleware.after do
17
+ if response.empty? && response.headers.any?
18
+ env["rodauth.rails.headers"] = response.headers
19
+ end
20
+ end
21
+
22
+ middleware.class_eval do
23
+ def self.inspect
24
+ "#{superclass}::Middleware"
25
+ end
26
+
27
+ def inspect
28
+ "#<#{self.class.inspect} request=#{request.inspect} response=#{response.inspect}>"
29
+ end
20
30
  end
21
31
  end
22
32
  end
@@ -0,0 +1,40 @@
1
+ require "rodauth"
2
+ require "rodauth/rails/feature"
3
+
4
+ module Rodauth
5
+ module Rails
6
+ # Base auth class that applies some default configuration and supports
7
+ # multi-level inheritance.
8
+ class Auth < Rodauth::Auth
9
+ class << self
10
+ attr_writer :features
11
+ attr_writer :routes
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ def self.inherited(auth_class)
16
+ super
17
+ auth_class.roda_class = Rodauth::Rails.app
18
+ auth_class.features = features.dup
19
+ auth_class.routes = routes.dup
20
+ auth_class.route_hash = route_hash.dup
21
+ auth_class.configuration = configuration.clone
22
+ auth_class.configuration.instance_variable_set(:@auth, auth_class)
23
+ end
24
+
25
+ # apply default configuration
26
+ configure do
27
+ enable :rails
28
+
29
+ # database functions are more complex to set up, so disable them by default
30
+ use_database_authentication_functions? false
31
+
32
+ # avoid having to set deadline values in column default values
33
+ set_deadline_values? true
34
+
35
+ # use HMACs for additional security
36
+ hmac_secret { Rodauth::Rails.secret_key_base }
37
+ end
38
+ end
39
+ end
40
+ end