rodauth-rails 0.4.0 → 0.6.1

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 +177 -77
  4. data/lib/generators/rodauth/install_generator.rb +28 -18
  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 -18
  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 +23 -4
  30. data/lib/rodauth/rails/app.rb +3 -1
  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 +92 -25
  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"
@@ -46,35 +46,45 @@ module Rodauth
46
46
 
47
47
  private
48
48
 
49
- def db_migrate_path
50
- return "db/migrate" unless ActiveRecord.version >= Gem::Version.new("5.0")
51
-
52
- super
53
- end
54
-
55
- def migration_version
56
- if ActiveRecord.version >= Gem::Version.new("5.0")
57
- "[#{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
58
54
  end
59
55
  end
60
56
 
61
57
  def sequel_adapter
62
58
  case activerecord_adapter
63
- when "postgresql" then "postgres#{"ql" if RUBY_ENGINE == "jruby"}"
64
- when "mysql2" then "mysql#{"2" unless RUBY_ENGINE == "jruby"}"
65
- 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
66
64
  end
67
65
  end
68
66
 
69
- def activerecord_adapter
70
- 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
71
75
  end
72
76
 
73
77
  def api_only?
74
- return false if ::Rails.gem_version < Gem::Version.new("5.0")
78
+ return unless ::Rails.gem_version >= Gem::Version.new("5.0")
75
79
 
76
80
  ::Rails.application.config.api_only
77
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
78
88
  end
79
89
  end
80
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,6 +11,10 @@ class RodauthApp < Rodauth::Rails::App
11
11
  # http://rodauth.jeremyevans.net/documentation.html
12
12
 
13
13
  # ==> General
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
+
14
18
  # Specify the controller used for view rendering and CSRF verification.
15
19
  rails_controller { RodauthController }
16
20
 
@@ -53,31 +57,27 @@ class RodauthApp < Rodauth::Rails::App
53
57
 
54
58
  # ==> Emails
55
59
  # Uncomment the lines below once you've imported mailer views.
56
- # send_reset_password_email do
57
- # 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)
58
62
  # end
59
- # send_verify_account_email do
60
- # 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)
61
65
  # end
62
- # send_verify_login_change_email do |login|
63
- # 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)
64
68
  # end
65
- # send_password_changed_email do
66
- # mailer_send(:password_changed, email_to)
69
+ # create_password_changed_email do
70
+ # RodauthMailer.password_changed(email_to)
67
71
  # end
68
- # # send_email_auth_email do
69
- # # 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)
70
74
  # # end
71
- # # send_unlock_account_email do
72
- # # 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)
73
77
  # # end
74
- # auth_class_eval do
78
+ # send_email do |email|
75
79
  # # queue email delivery on the mailer after the transaction commits
76
- # def mailer_send(type, *args)
77
- # db.after_commit do
78
- # RodauthMailer.public_send(type, *args).deliver_later
79
- # end
80
- # end
80
+ # db.after_commit { email.deliver_later }
81
81
  # end
82
82
 
83
83
  # In the meantime you can tweak settings for emails created by Rodauth
@@ -1,8 +1,4 @@
1
1
  require "sequel/core"
2
2
 
3
3
  # initialize Sequel and have it reuse Active Record's database connection
4
- <% if RUBY_ENGINE == "jruby" -%>
5
- DB = Sequel.connect("jdbc:<%= sequel_adapter %>://", extensions: :activerecord_connection)
6
- <% else -%>
7
- DB = Sequel.<%= sequel_adapter %>(extensions: :activerecord_connection)
8
- <% end -%>
4
+ DB = Sequel.connect("<%= sequel_uri_scheme %>://", extensions: :activerecord_connection)