rodauth-rails 0.4.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +187 -52
  4. data/lib/generators/rodauth/install_generator.rb +28 -20
  5. data/lib/generators/rodauth/migration/account_expiration.erb +7 -0
  6. data/lib/generators/rodauth/migration/active_sessions.erb +7 -0
  7. data/lib/generators/rodauth/migration/audit_logging.erb +16 -0
  8. data/lib/generators/rodauth/migration/base.erb +19 -0
  9. data/lib/generators/rodauth/migration/disallow_password_reuse.erb +5 -0
  10. data/lib/generators/rodauth/migration/email_auth.erb +7 -0
  11. data/lib/generators/rodauth/migration/jwt_refresh.erb +7 -0
  12. data/lib/generators/rodauth/migration/lockout.erb +11 -0
  13. data/lib/generators/rodauth/migration/otp.erb +7 -0
  14. data/lib/generators/rodauth/migration/password_expiration.erb +5 -0
  15. data/lib/generators/rodauth/migration/recovery_codes.erb +6 -0
  16. data/lib/generators/rodauth/migration/remember.erb +6 -0
  17. data/lib/generators/rodauth/migration/reset_password.erb +7 -0
  18. data/lib/generators/rodauth/migration/single_session.erb +5 -0
  19. data/lib/generators/rodauth/migration/sms_codes.erb +8 -0
  20. data/lib/generators/rodauth/migration/verify_account.erb +7 -0
  21. data/lib/generators/rodauth/migration/verify_login_change.erb +7 -0
  22. data/lib/generators/rodauth/migration/webauthn.erb +12 -0
  23. data/lib/generators/rodauth/migration_generator.rb +32 -0
  24. data/lib/generators/rodauth/migration_helpers.rb +69 -0
  25. data/lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb +2 -1
  26. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +18 -20
  27. data/lib/generators/rodauth/templates/config/initializers/sequel.rb +1 -5
  28. data/lib/generators/rodauth/templates/db/migrate/create_rodauth.rb +2 -176
  29. data/lib/rodauth/rails.rb +33 -4
  30. data/lib/rodauth/rails/app.rb +4 -2
  31. data/lib/rodauth/rails/app/flash.rb +1 -1
  32. data/lib/rodauth/rails/app/middleware.rb +26 -0
  33. data/lib/rodauth/rails/feature.rb +98 -30
  34. data/lib/rodauth/rails/railtie.rb +11 -0
  35. data/lib/rodauth/rails/tasks.rake +28 -0
  36. data/lib/rodauth/rails/version.rb +1 -1
  37. data/rodauth-rails.gemspec +3 -3
  38. metadata +29 -7
@@ -1,6 +1,6 @@
1
1
  require "rails/generators/base"
2
2
  require "rails/generators/active_record/migration"
3
-
3
+ require "generators/rodauth/migration_helpers"
4
4
  require "securerandom"
5
5
 
6
6
  module Rodauth
@@ -8,6 +8,7 @@ module Rodauth
8
8
  module Generators
9
9
  class InstallGenerator < ::Rails::Generators::Base
10
10
  include ::ActiveRecord::Generators::Migration
11
+ include MigrationHelpers
11
12
 
12
13
  source_root "#{__dir__}/templates"
13
14
  namespace "rodauth:install"
@@ -15,7 +16,7 @@ module Rodauth
15
16
  def create_rodauth_migration
16
17
  return unless defined?(ActiveRecord::Base)
17
18
 
18
- migration_template "db/migrate/create_rodauth.rb", File.join(db_migrate_path, "create_rodauth.rb")
19
+ migration_template "db/migrate/create_rodauth.rb"
19
20
  end
20
21
 
21
22
  def create_rodauth_initializer
@@ -24,7 +25,6 @@ module Rodauth
24
25
 
25
26
  def create_sequel_initializer
26
27
  return unless defined?(ActiveRecord::Base)
27
- return unless %w[postgresql mysql2 sqlite3].include?(activerecord_adapter)
28
28
  return if defined?(Sequel) && !Sequel::DATABASES.empty?
29
29
 
30
30
  template "config/initializers/sequel.rb"
@@ -35,8 +35,6 @@ module Rodauth
35
35
  end
36
36
 
37
37
  def create_rodauth_controller
38
- return if api_only?
39
-
40
38
  template "app/controllers/rodauth_controller.rb"
41
39
  end
42
40
 
@@ -48,35 +46,45 @@ module Rodauth
48
46
 
49
47
  private
50
48
 
51
- def db_migrate_path
52
- return "db/migrate" unless ActiveRecord.version >= Gem::Version.new("5.0")
53
-
54
- super
55
- end
56
-
57
- def migration_version
58
- if ActiveRecord.version >= Gem::Version.new("5.0")
59
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
49
+ def sequel_uri_scheme
50
+ if RUBY_ENGINE == "jruby"
51
+ "jdbc:#{sequel_jdbc_subadapter}"
52
+ else
53
+ sequel_adapter
60
54
  end
61
55
  end
62
56
 
63
57
  def sequel_adapter
64
58
  case activerecord_adapter
65
- when "postgresql" then "postgres#{"ql" if RUBY_ENGINE == "jruby"}"
66
- when "mysql2" then "mysql#{"2" unless RUBY_ENGINE == "jruby"}"
67
- when "sqlite3" then "sqlite"
59
+ when "sqlite3" then "sqlite"
60
+ when "oracle_enhanced" then "oracle" # https://github.com/rsim/oracle-enhanced
61
+ when "sqlserver" then "tinytds" # https://github.com/rails-sqlserver/activerecord-sqlserver-adapter
62
+ else
63
+ activerecord_adapter
68
64
  end
69
65
  end
70
66
 
71
- def activerecord_adapter
72
- ActiveRecord::Base.connection_config.fetch(:adapter)
67
+ def sequel_jdbc_subadapter
68
+ case activerecord_adapter
69
+ when "sqlite3" then "sqlite"
70
+ when "oracle_enhanced" then "oracle" # https://github.com/rsim/oracle-enhanced
71
+ when "sqlserver" then "mssql"
72
+ else
73
+ activerecord_adapter
74
+ end
73
75
  end
74
76
 
75
77
  def api_only?
76
- return false if ::Rails.gem_version < Gem::Version.new("5.0")
78
+ return unless ::Rails.gem_version >= Gem::Version.new("5.0")
77
79
 
78
80
  ::Rails.application.config.api_only
79
81
  end
82
+
83
+ def migration_features
84
+ features = [:base, :reset_password, :verify_account, :verify_login_change]
85
+ features << :remember unless api_only?
86
+ features
87
+ end
80
88
  end
81
89
  end
82
90
  end
@@ -0,0 +1,7 @@
1
+ # Used by the account expiration feature
2
+ create_table :account_activity_times<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.datetime :last_activity_at, null: false
5
+ t.datetime :last_login_at, null: false
6
+ t.datetime :expired_at
7
+ end
@@ -0,0 +1,7 @@
1
+ # Used by the active sessions feature
2
+ create_table :account_active_session_keys, primary_key: [:account_id, :session_id] do |t|
3
+ t.references :account, foreign_key: true<%= primary_key_type(:type) %>
4
+ t.string :session_id
5
+ t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
6
+ t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
7
+ end
@@ -0,0 +1,16 @@
1
+ # Used by the audit logging feature
2
+ create_table :account_authentication_audit_logs<%= primary_key_type %> do |t|
3
+ t.references :account, foreign_key: true, null: false<%= primary_key_type(:type) %>
4
+ t.datetime :at, null: false, default: -> { "CURRENT_TIMESTAMP" }
5
+ t.text :message, null: false
6
+ <% case activerecord_adapter -%>
7
+ <% when "postgresql" -%>
8
+ t.jsonb :metadata
9
+ <% when "sqlite3", "mysql2" -%>
10
+ t.json :metadata
11
+ <% else -%>
12
+ t.string :metadata
13
+ <% end -%>
14
+ t.index [:account_id, :at], name: "audit_account_at_idx"
15
+ t.index :at, name: "audit_at_idx"
16
+ end
@@ -0,0 +1,19 @@
1
+ <% if activerecord_adapter == "postgresql" -%>
2
+ enable_extension "citext"
3
+
4
+ <% end -%>
5
+ create_table :accounts<%= primary_key_type %> do |t|
6
+ <% case activerecord_adapter -%>
7
+ <% when "postgresql" -%>
8
+ t.citext :email, null: false, index: { unique: true, where: "status IN ('verified', 'unverified')" }
9
+ <% else -%>
10
+ t.string :email, null: false, index: { unique: true }
11
+ <% end -%>
12
+ t.string :status, null: false, default: "verified"
13
+ end
14
+
15
+ # Used if storing password hashes in a separate table (default)
16
+ create_table :account_password_hashes<%= primary_key_type %> do |t|
17
+ t.foreign_key :accounts, column: :id
18
+ t.string :password_hash, null: false
19
+ end
@@ -0,0 +1,5 @@
1
+ # Used by the disallow_password_reuse feature
2
+ create_table :account_previous_password_hashes do |t|
3
+ t.references :account, foreign_key: true<%= primary_key_type(:type) %>
4
+ t.string :password_hash, null: false
5
+ end
@@ -0,0 +1,7 @@
1
+ # Used by the email auth feature
2
+ create_table :account_email_auth_keys<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.string :key, null: false
5
+ t.datetime :deadline, null: false
6
+ t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
7
+ end
@@ -0,0 +1,7 @@
1
+ # Used by the jwt refresh feature
2
+ create_table :account_jwt_refresh_keys<%= primary_key_type %> do |t|
3
+ t.references :account, foreign_key: true, null: false<%= primary_key_type(:type) %>
4
+ t.string :key, null: false
5
+ t.datetime :deadline, null: false
6
+ t.index :account_id, name: "account_jwt_rk_account_id_idx"
7
+ end
@@ -0,0 +1,11 @@
1
+ # Used by the lockout feature
2
+ create_table :account_login_failures<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.integer :number, null: false, default: 1
5
+ end
6
+ create_table :account_lockouts<%= primary_key_type %> do |t|
7
+ t.foreign_key :accounts, column: :id
8
+ t.string :key, null: false
9
+ t.datetime :deadline, null: false
10
+ t.datetime :email_last_sent
11
+ end
@@ -0,0 +1,7 @@
1
+ # Used by the otp feature
2
+ create_table :account_otp_keys<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.string :key, null: false
5
+ t.integer :num_failures, null: false, default: 0
6
+ t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
7
+ end
@@ -0,0 +1,5 @@
1
+ # Used by the password expiration feature
2
+ create_table :account_password_change_times<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.datetime :changed_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
5
+ end
@@ -0,0 +1,6 @@
1
+ # Used by the recovery codes feature
2
+ create_table :account_recovery_codes, primary_key: [:id, :code] do |t|
3
+ t.column :id, :<%= primary_key_type(nil) || :bigint %>
4
+ t.foreign_key :accounts, column: :id
5
+ t.string :code
6
+ end
@@ -0,0 +1,6 @@
1
+ # Used by the remember me feature
2
+ create_table :account_remember_keys<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.string :key, null: false
5
+ t.datetime :deadline, null: false
6
+ end
@@ -0,0 +1,7 @@
1
+ # Used by the password reset feature
2
+ create_table :account_password_reset_keys<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.string :key, null: false
5
+ t.datetime :deadline, null: false
6
+ t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
7
+ end
@@ -0,0 +1,5 @@
1
+ # Used by the single session feature
2
+ create_table :account_session_keys<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.string :key, null: false
5
+ end
@@ -0,0 +1,8 @@
1
+ # Used by the sms codes feature
2
+ create_table :account_sms_codes<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.string :phone_number, null: false
5
+ t.integer :num_failures
6
+ t.string :code
7
+ t.datetime :code_issued_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
8
+ end
@@ -0,0 +1,7 @@
1
+ # Used by the account verification feature
2
+ create_table :account_verification_keys<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.string :key, null: false
5
+ t.datetime :requested_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
6
+ t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
7
+ end
@@ -0,0 +1,7 @@
1
+ # Used by the verify login change feature
2
+ create_table :account_login_change_keys<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.string :key, null: false
5
+ t.string :login, null: false
6
+ t.datetime :deadline, null: false
7
+ end
@@ -0,0 +1,12 @@
1
+ # Used by the webauthn feature
2
+ create_table :account_webauthn_user_ids<%= primary_key_type %> do |t|
3
+ t.foreign_key :accounts, column: :id
4
+ t.string :webauthn_id, null: false
5
+ end
6
+ create_table :account_webauthn_keys, primary_key: [:account_id, :webauthn_id] do |t|
7
+ t.references :account, foreign_key: true<%= primary_key_type(:type) %>
8
+ t.string :webauthn_id
9
+ t.string :public_key, null: false
10
+ t.integer :sign_count, null: false
11
+ t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
12
+ end
@@ -0,0 +1,32 @@
1
+ require "rails/generators/base"
2
+ require "rails/generators/active_record/migration"
3
+ require "generators/rodauth/migration_helpers"
4
+
5
+ module Rodauth
6
+ module Rails
7
+ module Generators
8
+ class MigrationGenerator < ::Rails::Generators::Base
9
+ include ::ActiveRecord::Generators::Migration
10
+ include MigrationHelpers
11
+
12
+ source_root "#{__dir__}/templates"
13
+ namespace "rodauth:migration"
14
+
15
+ argument :features, optional: true, type: :array,
16
+ desc: "Rodauth features to create tables for (otp, sms_codes, single_session, account_expiration etc.)",
17
+ default: %w[]
18
+
19
+ def create_rodauth_migration
20
+ return unless defined?(ActiveRecord::Base)
21
+ return if features.empty?
22
+
23
+ migration_template "db/migrate/create_rodauth.rb", "create_rodauth_#{features.join("_")}.rb"
24
+ end
25
+
26
+ def migration_features
27
+ features
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,69 @@
1
+ require "erb"
2
+
3
+ module Rodauth
4
+ module Rails
5
+ module Generators
6
+ module MigrationHelpers
7
+ attr_reader :migration_class_name
8
+
9
+ def migration_template(source, destination = File.basename(source))
10
+ @migration_class_name = destination.chomp(".rb").camelize
11
+
12
+ super source, File.join(db_migrate_path, destination)
13
+ end
14
+
15
+ private
16
+
17
+ def migration_content
18
+ migration_features
19
+ .select { |feature| File.exist?("#{__dir__}/migration/#{feature}.erb") }
20
+ .map { |feature| File.read("#{__dir__}/migration/#{feature}.erb") }
21
+ .map { |content| erb_eval(content) }
22
+ .join("\n")
23
+ .indent(4)
24
+ end
25
+
26
+ def activerecord_adapter
27
+ if ActiveRecord::Base.respond_to?(:connection_db_config)
28
+ ActiveRecord::Base.connection_db_config.adapter
29
+ else
30
+ ActiveRecord::Base.connection_config.fetch(:adapter)
31
+ end
32
+ end
33
+
34
+ def migration_version
35
+ return unless ActiveRecord.version >= Gem::Version.new("5.0")
36
+
37
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
38
+ end
39
+
40
+ def db_migrate_path
41
+ return "db/migrate" unless ActiveRecord.version >= Gem::Version.new("5.0")
42
+
43
+ super
44
+ end
45
+
46
+ def primary_key_type(key = :id)
47
+ generators = ::Rails.application.config.generators
48
+ column_type = generators.options[:active_record][:primary_key_type]
49
+
50
+ return unless column_type
51
+
52
+ if key
53
+ ", #{key}: :#{column_type}"
54
+ else
55
+ column_type
56
+ end
57
+ end
58
+
59
+ def erb_eval(content)
60
+ if ERB.version[/\d+\.\d+\.\d+/].to_s >= "2.2.0"
61
+ ERB.new(content, trim_mode: "-").result(binding)
62
+ else
63
+ ERB.new(content, 0, "-").result(binding)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,3 +1,4 @@
1
1
  class RodauthController < ApplicationController
2
- # used by Rodauth for rendering views and CSRF protection
2
+ # used by Rodauth for rendering views, CSRF protection, and running any
3
+ # registered action callbacks and rescue_from handlers
3
4
  end
@@ -11,10 +11,12 @@ class RodauthApp < Rodauth::Rails::App
11
11
  # http://rodauth.jeremyevans.net/documentation.html
12
12
 
13
13
  # ==> General
14
- <% unless api_only? -%>
14
+ # The secret key used for hashing public-facing tokens for various features.
15
+ # Defaults to Rails `secret_key_base`, but you can use your own secret key.
16
+ # hmac_secret "<%= SecureRandom.hex(64) %>"
17
+
15
18
  # Specify the controller used for view rendering and CSRF verification.
16
19
  rails_controller { RodauthController }
17
- <% end -%>
18
20
 
19
21
  # Store account status in a text column.
20
22
  account_status_column :status
@@ -55,31 +57,27 @@ class RodauthApp < Rodauth::Rails::App
55
57
 
56
58
  # ==> Emails
57
59
  # Uncomment the lines below once you've imported mailer views.
58
- # send_reset_password_email do
59
- # mailer_send(:reset_password, email_to, reset_password_email_link)
60
+ # create_reset_password_email do
61
+ # RodauthMailer.reset_password(email_to, reset_password_email_link)
60
62
  # end
61
- # send_verify_account_email do
62
- # mailer_send(:verify_account, email_to, verify_account_email_link)
63
+ # create_verify_account_email do
64
+ # RodauthMailer.verify_account(email_to, verify_account_email_link)
63
65
  # end
64
- # send_verify_login_change_email do |login|
65
- # mailer_send(:verify_login_change, login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
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)
66
68
  # end
67
- # send_password_changed_email do
68
- # mailer_send(:password_changed, email_to)
69
+ # create_password_changed_email do
70
+ # RodauthMailer.password_changed(email_to)
69
71
  # end
70
- # # send_email_auth_email do
71
- # # mailer_send(:email_auth, email_to, email_auth_email_link)
72
+ # # create_email_auth_email do
73
+ # # RodauthMailer.email_auth(email_to, email_auth_email_link)
72
74
  # # end
73
- # # send_unlock_account_email do
74
- # # mailer_send(:unlock_account, email_to, unlock_account_email_link)
75
+ # # create_unlock_account_email do
76
+ # # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
75
77
  # # end
76
- # auth_class_eval do
78
+ # send_email do |email|
77
79
  # # queue email delivery on the mailer after the transaction commits
78
- # def mailer_send(type, *args)
79
- # db.after_commit do
80
- # RodauthMailer.public_send(type, *args).deliver_later
81
- # end
82
- # end
80
+ # db.after_commit { email.deliver_later }
83
81
  # end
84
82
 
85
83
  # In the meantime you can tweak settings for emails created by Rodauth