rodauth-rails 0.3.1 → 0.6.0

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +169 -69
  4. data/lib/generators/rodauth/install_generator.rb +34 -17
  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/{lib → app/lib}/rodauth_app.rb +26 -2
  26. data/lib/generators/rodauth/templates/config/initializers/sequel.rb +1 -5
  27. data/lib/generators/rodauth/templates/db/migrate/create_rodauth.rb +2 -167
  28. data/lib/rodauth/rails.rb +24 -5
  29. data/lib/rodauth/rails/app.rb +5 -4
  30. data/lib/rodauth/rails/feature.rb +69 -13
  31. data/lib/rodauth/rails/flash.rb +48 -0
  32. data/lib/rodauth/rails/railtie.rb +11 -0
  33. data/lib/rodauth/rails/tasks.rake +28 -0
  34. data/lib/rodauth/rails/version.rb +5 -0
  35. data/rodauth-rails.gemspec +6 -4
  36. metadata +31 -9
  37. data/lib/rodauth/rails/app/flash.rb +0 -50
@@ -1,11 +1,14 @@
1
1
  require "rails/generators/base"
2
2
  require "rails/generators/active_record/migration"
3
+ require "generators/rodauth/migration_helpers"
4
+ require "securerandom"
3
5
 
4
6
  module Rodauth
5
7
  module Rails
6
8
  module Generators
7
9
  class InstallGenerator < ::Rails::Generators::Base
8
10
  include ::ActiveRecord::Generators::Migration
11
+ include MigrationHelpers
9
12
 
10
13
  source_root "#{__dir__}/templates"
11
14
  namespace "rodauth:install"
@@ -13,7 +16,7 @@ module Rodauth
13
16
  def create_rodauth_migration
14
17
  return unless defined?(ActiveRecord::Base)
15
18
 
16
- migration_template "db/migrate/create_rodauth.rb", File.join(db_migrate_path, "create_rodauth.rb")
19
+ migration_template "db/migrate/create_rodauth.rb"
17
20
  end
18
21
 
19
22
  def create_rodauth_initializer
@@ -22,17 +25,18 @@ module Rodauth
22
25
 
23
26
  def create_sequel_initializer
24
27
  return unless defined?(ActiveRecord::Base)
25
- return unless %w[postgresql mysql2 sqlite3].include?(activerecord_adapter)
26
28
  return if defined?(Sequel) && !Sequel::DATABASES.empty?
27
29
 
28
30
  template "config/initializers/sequel.rb"
29
31
  end
30
32
 
31
33
  def create_rodauth_app
32
- template "lib/rodauth_app.rb"
34
+ template "app/lib/rodauth_app.rb"
33
35
  end
34
36
 
35
37
  def create_rodauth_controller
38
+ return if api_only?
39
+
36
40
  template "app/controllers/rodauth_controller.rb"
37
41
  end
38
42
 
@@ -44,31 +48,44 @@ module Rodauth
44
48
 
45
49
  private
46
50
 
47
- def db_migrate_path
48
- return "db/migrate" unless activerecord_at_least?(5, 0)
49
- super
51
+ def sequel_uri_scheme
52
+ if RUBY_ENGINE == "jruby"
53
+ "jdbc:#{sequel_jdbc_subadapter}"
54
+ else
55
+ sequel_adapter
56
+ end
50
57
  end
51
58
 
52
- def migration_version
53
- if activerecord_at_least?(5, 0)
54
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
59
+ def sequel_adapter
60
+ case activerecord_adapter
61
+ when "sqlite3" then "sqlite"
62
+ when "oracle_enhanced" then "oracle" # https://github.com/rsim/oracle-enhanced
63
+ when "sqlserver" then "tinytds" # https://github.com/rails-sqlserver/activerecord-sqlserver-adapter
64
+ else
65
+ activerecord_adapter
55
66
  end
56
67
  end
57
68
 
58
- def sequel_adapter
69
+ def sequel_jdbc_subadapter
59
70
  case activerecord_adapter
60
- when "postgresql" then "postgres#{"ql" if RUBY_ENGINE == "jruby"}"
61
- when "mysql2" then "mysql#{"2" unless RUBY_ENGINE == "jruby"}"
62
- when "sqlite3" then "sqlite"
71
+ when "sqlite3" then "sqlite"
72
+ when "oracle_enhanced" then "oracle" # https://github.com/rsim/oracle-enhanced
73
+ when "sqlserver" then "mssql"
74
+ else
75
+ activerecord_adapter
63
76
  end
64
77
  end
65
78
 
66
- def activerecord_adapter
67
- ActiveRecord::Base.connection_config.fetch(:adapter)
79
+ def api_only?
80
+ return unless ::Rails.gem_version >= Gem::Version.new("5.0")
81
+
82
+ ::Rails.application.config.api_only
68
83
  end
69
84
 
70
- def activerecord_at_least?(major, minor)
71
- ActiveRecord.version >= Gem::Version.new("#{major}.#{minor}")
85
+ def migration_features
86
+ features = [:base, :reset_password, :verify_account, :verify_login_change]
87
+ features << :remember unless api_only?
88
+ features
72
89
  end
73
90
  end
74
91
  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,8 +1,8 @@
1
1
  class RodauthApp < Rodauth::Rails::App
2
- configure do
2
+ configure<%= " json: :only" if api_only? %> do
3
3
  # List of authentication features that are loaded.
4
4
  enable :create_account, :verify_account, :verify_account_grace_period,
5
- :login, :remember, :logout,
5
+ :login, :logout, <%= api_only? ? ":jwt" : ":remember" %>,
6
6
  :reset_password, :change_password, :change_password_notify,
7
7
  :change_login, :verify_login_change,
8
8
  :close_account
@@ -11,9 +11,15 @@ 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
+
18
+ <% unless api_only? -%>
14
19
  # Specify the controller used for view rendering and CSRF verification.
15
20
  rails_controller { RodauthController }
16
21
 
22
+ <% end -%>
17
23
  # Store account status in a text column.
18
24
  account_status_column :status
19
25
  account_unverified_status_value "unverified"
@@ -38,6 +44,18 @@ class RodauthApp < Rodauth::Rails::App
38
44
 
39
45
  # Redirect to the app from login and registration pages if already logged in.
40
46
  # already_logged_in { redirect login_redirect }
47
+ <% if api_only? -%>
48
+
49
+ # ==> JWT
50
+ # Set JWT secret, which is used to cryptographically protect the token.
51
+ jwt_secret "<%= SecureRandom.hex(64) %>"
52
+
53
+ # Don't require login confirmation param.
54
+ require_login_confirmation? false
55
+
56
+ # Don't require password confirmation param.
57
+ require_password_confirmation? false
58
+ <% end -%>
41
59
 
42
60
  # ==> Emails
43
61
  # Uncomment the lines below once you've imported mailer views.
@@ -75,10 +93,12 @@ class RodauthApp < Rodauth::Rails::App
75
93
  # reset_password_email_body { "Click here to reset your password: #{reset_password_email_link}" }
76
94
 
77
95
  # ==> Flash
96
+ <% unless api_only? -%>
78
97
  # Match flash keys with ones already used in the Rails app.
79
98
  # flash_notice_key :success # default is :notice
80
99
  # flash_error_key :error # default is :alert
81
100
 
101
+ <% end -%>
82
102
  # Override default flash messages.
83
103
  # create_account_notice_flash "Your account has been created. Please verify your account by visiting the confirmation link sent to your email address."
84
104
  # require_login_error_flash "Login is required for accessing this page"
@@ -93,6 +113,7 @@ class RodauthApp < Rodauth::Rails::App
93
113
 
94
114
  # Change minimum number of password characters required when creating an account.
95
115
  # password_minimum_length 8
116
+ <% unless api_only? -%>
96
117
 
97
118
  # ==> Remember Feature
98
119
  # Remember all logged in users.
@@ -103,6 +124,7 @@ class RodauthApp < Rodauth::Rails::App
103
124
 
104
125
  # Extend user's remember period when remembered via a cookie
105
126
  extend_remember_deadline? true
127
+ <% end -%>
106
128
 
107
129
  # ==> Hooks
108
130
  # Validate custom fields in the create account form.
@@ -147,8 +169,10 @@ class RodauthApp < Rodauth::Rails::App
147
169
  # end
148
170
 
149
171
  route do |r|
172
+ <% unless api_only? -%>
150
173
  rodauth.load_memory # autologin remembered users
151
174
 
175
+ <% end -%>
152
176
  r.rodauth # route rodauth requests
153
177
 
154
178
  # ==> Authenticating Requests