├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── gemfiles ├── Gemfile.rails-5.1 ├── Gemfile.rails-5.2 ├── Gemfile.rails-6.0 ├── Gemfile.rails-6.1 ├── Gemfile.rails-7.0 ├── Gemfile.rails-7.1 ├── Gemfile.rails-7.2 └── Gemfile.rails-8.0 ├── lib ├── generators │ └── rodauth │ │ ├── install_generator.rb │ │ ├── mailer │ │ ├── email_auth.erb │ │ ├── otp_disabled.erb │ │ ├── otp_locked_out.erb │ │ ├── otp_setup.erb │ │ ├── otp_unlock_failed.erb │ │ ├── otp_unlocked.erb │ │ ├── password_changed.erb │ │ ├── reset_password.erb │ │ ├── reset_password_notify.erb │ │ ├── unlock_account.erb │ │ ├── verify_account.erb │ │ ├── verify_login_change.erb │ │ ├── webauthn_authenticator_added.erb │ │ └── webauthn_authenticator_removed.erb │ │ ├── mailer_generator.rb │ │ ├── migration │ │ ├── active_record │ │ │ ├── account_expiration.erb │ │ │ ├── active_sessions.erb │ │ │ ├── audit_logging.erb │ │ │ ├── base.erb │ │ │ ├── disallow_password_reuse.erb │ │ │ ├── email_auth.erb │ │ │ ├── jwt_refresh.erb │ │ │ ├── lockout.erb │ │ │ ├── otp.erb │ │ │ ├── otp_unlock.erb │ │ │ ├── password_expiration.erb │ │ │ ├── recovery_codes.erb │ │ │ ├── remember.erb │ │ │ ├── reset_password.erb │ │ │ ├── single_session.erb │ │ │ ├── sms_codes.erb │ │ │ ├── verify_account.erb │ │ │ ├── verify_login_change.erb │ │ │ └── webauthn.erb │ │ └── sequel │ │ │ ├── account_expiration.erb │ │ │ ├── active_sessions.erb │ │ │ ├── audit_logging.erb │ │ │ ├── base.erb │ │ │ ├── disallow_password_reuse.erb │ │ │ ├── email_auth.erb │ │ │ ├── jwt_refresh.erb │ │ │ ├── lockout.erb │ │ │ ├── otp.erb │ │ │ ├── otp_unlock.erb │ │ │ ├── password_expiration.erb │ │ │ ├── recovery_codes.erb │ │ │ ├── remember.erb │ │ │ ├── reset_password.erb │ │ │ ├── single_session.erb │ │ │ ├── sms_codes.erb │ │ │ ├── verify_account.erb │ │ │ ├── verify_login_change.erb │ │ │ └── webauthn.erb │ │ ├── migration_generator.rb │ │ ├── templates │ │ ├── INSTRUCTIONS │ │ ├── app │ │ │ ├── controllers │ │ │ │ └── rodauth_controller.rb.tt │ │ │ ├── mailers │ │ │ │ └── rodauth_mailer.rb.tt │ │ │ ├── misc │ │ │ │ ├── rodauth_app.rb.tt │ │ │ │ └── rodauth_main.rb.tt │ │ │ ├── models │ │ │ │ └── account.rb.tt │ │ │ └── views │ │ │ │ ├── rodauth │ │ │ │ ├── _email_auth_request_form.html.erb │ │ │ │ ├── _login_form.html.erb │ │ │ │ ├── _login_form_footer.html.erb │ │ │ │ ├── add_recovery_codes.html.erb │ │ │ │ ├── change_login.html.erb │ │ │ │ ├── change_password.html.erb │ │ │ │ ├── close_account.html.erb │ │ │ │ ├── confirm_password.html.erb │ │ │ │ ├── create_account.html.erb │ │ │ │ ├── email_auth.html.erb │ │ │ │ ├── login.html.erb │ │ │ │ ├── logout.html.erb │ │ │ │ ├── multi_phase_login.html.erb │ │ │ │ ├── otp_auth.html.erb │ │ │ │ ├── otp_disable.html.erb │ │ │ │ ├── otp_setup.html.erb │ │ │ │ ├── otp_unlock.html.erb │ │ │ │ ├── otp_unlock_not_available.html.erb │ │ │ │ ├── recovery_auth.html.erb │ │ │ │ ├── recovery_codes.html.erb │ │ │ │ ├── remember.html.erb │ │ │ │ ├── reset_password.html.erb │ │ │ │ ├── reset_password_request.html.erb │ │ │ │ ├── sms_auth.html.erb │ │ │ │ ├── sms_confirm.html.erb │ │ │ │ ├── sms_disable.html.erb │ │ │ │ ├── sms_request.html.erb │ │ │ │ ├── sms_setup.html.erb │ │ │ │ ├── tailwind │ │ │ │ │ ├── _email_auth_request_form.html.erb │ │ │ │ │ ├── _login_form.html.erb │ │ │ │ │ ├── _login_form_footer.html.erb │ │ │ │ │ ├── add_recovery_codes.html.erb │ │ │ │ │ ├── change_login.html.erb │ │ │ │ │ ├── change_password.html.erb │ │ │ │ │ ├── close_account.html.erb │ │ │ │ │ ├── confirm_password.html.erb │ │ │ │ │ ├── create_account.html.erb │ │ │ │ │ ├── email_auth.html.erb │ │ │ │ │ ├── login.html.erb │ │ │ │ │ ├── logout.html.erb │ │ │ │ │ ├── multi_phase_login.html.erb │ │ │ │ │ ├── otp_auth.html.erb │ │ │ │ │ ├── otp_disable.html.erb │ │ │ │ │ ├── otp_setup.html.erb │ │ │ │ │ ├── otp_unlock.html.erb │ │ │ │ │ ├── otp_unlock_not_available.html.erb │ │ │ │ │ ├── recovery_auth.html.erb │ │ │ │ │ ├── recovery_codes.html.erb │ │ │ │ │ ├── remember.html.erb │ │ │ │ │ ├── reset_password.html.erb │ │ │ │ │ ├── reset_password_request.html.erb │ │ │ │ │ ├── sms_auth.html.erb │ │ │ │ │ ├── sms_confirm.html.erb │ │ │ │ │ ├── sms_disable.html.erb │ │ │ │ │ ├── sms_request.html.erb │ │ │ │ │ ├── sms_setup.html.erb │ │ │ │ │ ├── two_factor_auth.html.erb │ │ │ │ │ ├── two_factor_disable.html.erb │ │ │ │ │ ├── two_factor_manage.html.erb │ │ │ │ │ ├── unlock_account.html.erb │ │ │ │ │ ├── unlock_account_request.html.erb │ │ │ │ │ ├── verify_account.html.erb │ │ │ │ │ ├── verify_account_resend.html.erb │ │ │ │ │ ├── verify_login_change.html.erb │ │ │ │ │ ├── webauthn_auth.html.erb │ │ │ │ │ ├── webauthn_autofill.html.erb │ │ │ │ │ ├── webauthn_remove.html.erb │ │ │ │ │ └── webauthn_setup.html.erb │ │ │ │ ├── two_factor_auth.html.erb │ │ │ │ ├── two_factor_disable.html.erb │ │ │ │ ├── two_factor_manage.html.erb │ │ │ │ ├── unlock_account.html.erb │ │ │ │ ├── unlock_account_request.html.erb │ │ │ │ ├── verify_account.html.erb │ │ │ │ ├── verify_account_resend.html.erb │ │ │ │ ├── verify_login_change.html.erb │ │ │ │ ├── webauthn_auth.html.erb │ │ │ │ ├── webauthn_autofill.html.erb │ │ │ │ ├── webauthn_remove.html.erb │ │ │ │ └── webauthn_setup.html.erb │ │ │ │ └── rodauth_mailer │ │ │ │ ├── email_auth.text.erb │ │ │ │ ├── otp_disabled.text.erb │ │ │ │ ├── otp_locked_out.text.erb │ │ │ │ ├── otp_setup.text.erb │ │ │ │ ├── otp_unlock_failed.text.erb │ │ │ │ ├── otp_unlocked.text.erb │ │ │ │ ├── password_changed.text.erb │ │ │ │ ├── reset_password.text.erb │ │ │ │ ├── reset_password_notify.text.erb │ │ │ │ ├── unlock_account.text.erb │ │ │ │ ├── verify_account.text.erb │ │ │ │ ├── verify_login_change.text.erb │ │ │ │ ├── webauthn_authenticator_added.text.erb │ │ │ │ └── webauthn_authenticator_removed.text.erb │ │ ├── config │ │ │ └── initializers │ │ │ │ └── rodauth.rb.tt │ │ ├── db │ │ │ └── migrate │ │ │ │ └── create_rodauth.rb.tt │ │ └── test │ │ │ └── fixtures │ │ │ └── accounts.yml.tt │ │ └── views_generator.rb ├── rodauth-rails.rb └── rodauth │ ├── rails.rb │ └── rails │ ├── app.rb │ ├── auth.rb │ ├── controller_methods.rb │ ├── feature.rb │ ├── feature │ ├── base.rb │ ├── callbacks.rb │ ├── csrf.rb │ ├── email.rb │ ├── instrumentation.rb │ ├── internal_request.rb │ └── render.rb │ ├── mailer.rb │ ├── middleware.rb │ ├── railtie.rb │ ├── tasks.rake │ ├── tasks │ └── routes.rb │ ├── test.rb │ ├── test │ └── controller.rb │ └── version.rb ├── rodauth-rails.gemspec └── test ├── controllers └── test_controller_test.rb ├── generators ├── install_generator_test.rb ├── mailer_generator_test.rb ├── migration_generator_test.rb └── views_generator_test.rb ├── integration ├── assets_test.rb ├── callbacks_test.rb ├── configurations_test.rb ├── constraint_test.rb ├── controller_test.rb ├── csrf_test.rb ├── email_test.rb ├── flash_test.rb ├── headers_test.rb ├── instrumentation_test.rb ├── json_test.rb ├── model_test.rb ├── redirect_test.rb ├── render_test.rb ├── rescue_test.rb └── session_test.rb ├── internal_request_test.rb ├── model_mixin_test.rb ├── rails_app ├── Rakefile ├── app │ ├── controllers │ │ ├── admin │ │ │ └── rodauth_controller.rb │ │ ├── application_controller.rb │ │ ├── rodauth_controller.rb │ │ └── test_controller.rb │ ├── misc │ │ ├── rodauth_admin.rb │ │ ├── rodauth_app.rb │ │ └── rodauth_main.rb │ ├── models │ │ ├── account.rb │ │ └── application_record.rb │ └── views │ │ ├── layouts │ │ └── application.html.erb │ │ ├── rodauth │ │ ├── _global_logout_field.html.erb │ │ ├── close_account.html.erb │ │ ├── reset_password_request.html.erb │ │ └── verify_account.html.erb │ │ └── test │ │ └── template.html.erb ├── config │ ├── application.rb │ ├── database.yml │ ├── environment.rb │ ├── initializers │ │ ├── rodauth.rb │ │ └── sequel.rb │ └── routes.rb └── db │ └── migrate │ └── 20200411171322_create_rodauth.rb ├── rake_test.rb ├── rodauth_test.rb └── test_helper.rb /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: janko 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ '**' ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | tests: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | ruby: ["ruby-2.6", "ruby-2.7", "ruby-3.0", "ruby-3.1", "ruby-3.2", "ruby-3.3", "ruby-3.4", "jruby-9.4"] 19 | gemfile: ["rails-5.1", "rails-5.2", "rails-6.0", "rails-6.1", "rails-7.0", "rails-7.1", "rails-7.2", "rails-8.0"] 20 | exclude: 21 | - ruby: "ruby-3.4" 22 | gemfile: "rails-5.2" 23 | - ruby: "ruby-3.4" 24 | gemfile: "rails-5.1" 25 | - ruby: "ruby-3.3" 26 | gemfile: "rails-5.2" 27 | - ruby: "ruby-3.3" 28 | gemfile: "rails-5.1" 29 | - ruby: "ruby-3.2" 30 | gemfile: "rails-5.2" 31 | - ruby: "ruby-3.2" 32 | gemfile: "rails-5.1" 33 | - ruby: "ruby-3.1" 34 | gemfile: "rails-8.0" 35 | - ruby: "ruby-3.1" 36 | gemfile: "rails-5.2" 37 | - ruby: "ruby-3.1" 38 | gemfile: "rails-5.1" 39 | - ruby: "ruby-3.0" 40 | gemfile: "rails-8.0" 41 | - ruby: "ruby-3.0" 42 | gemfile: "rails-7.2" 43 | - ruby: "ruby-3.0" 44 | gemfile: "rails-5.2" 45 | - ruby: "ruby-3.0" 46 | gemfile: "rails-5.1" 47 | - ruby: "ruby-2.7" 48 | gemfile: "rails-8.0" 49 | - ruby: "ruby-2.7" 50 | gemfile: "rails-7.2" 51 | - ruby: "jruby-9.4" 52 | gemfile: "rails-8.0" 53 | - ruby: "jruby-9.4" 54 | gemfile: "rails-7.2" 55 | - ruby: "jruby-9.4" 56 | gemfile: "rails-7.1" 57 | - ruby: "jruby-9.4" 58 | gemfile: "rails-5.2" 59 | - ruby: "jruby-9.4" 60 | gemfile: "rails-5.1" 61 | - ruby: "ruby-2.6" 62 | gemfile: "rails-8.0" 63 | - ruby: "ruby-2.6" 64 | gemfile: "rails-7.2" 65 | - ruby: "ruby-2.6" 66 | gemfile: "rails-7.1" 67 | - ruby: "ruby-2.6" 68 | gemfile: "rails-7.0" 69 | env: 70 | BUNDLE_GEMFILE: gemfiles/Gemfile.${{ matrix.gemfile }} 71 | 72 | steps: 73 | - uses: actions/checkout@v4 74 | 75 | - name: Set up Ruby 76 | uses: ruby/setup-ruby@v1 77 | with: 78 | ruby-version: ${{ matrix.ruby }} 79 | bundler-cache: true 80 | 81 | - name: Run tests 82 | run: bundle exec rake test 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | gemfiles/*.lock 3 | pkg/ 4 | database.sqlite3 5 | tmp/ 6 | .ruby-version 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | The changelog is tracked in [GitHub releases](https://github.com/janko/rodauth-rails/releases). 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "sequel-activerecord_connection", "~> 2.0" 6 | 7 | gem "rake", "~> 13.0" 8 | gem "warning" 9 | 10 | gem "rails", "~> 8.0" 11 | gem "turbo-rails", "~> 1.4" 12 | gem "sqlite3", "~> 2.0", platforms: [:mri, :truffleruby] 13 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 14 | 15 | gem "capybara" 16 | 17 | if RUBY_VERSION >= "3.1.0" 18 | # mail gem dependencies on Ruby 3.1+ 19 | gem "net-smtp" 20 | gem "net-imap" 21 | gem "net-pop" 22 | 23 | # rake gem dependency on Ruby 3.1+ 24 | gem "matrix" 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2023 Janko Marohnić 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.pattern = "test/**/*_test.rb" 7 | t.warning = true 8 | end 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-5.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "sequel-activerecord_connection", "~> 2.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | gem "rake", "~> 12.0" 9 | gem "warning" 10 | 11 | gem "rails", "~> 5.1.0" 12 | gem "sqlite3", "~> 1.4", platforms: :mri 13 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 14 | 15 | gem "capybara" 16 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-5.2: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "sequel-activerecord_connection", "~> 2.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | gem "rake", "~> 12.0" 9 | gem "warning" 10 | 11 | gem "rails", "~> 5.2.0" 12 | gem "sqlite3", "~> 1.4", platforms: :mri 13 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 14 | 15 | gem "capybara" 16 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-6.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "sequel-activerecord_connection", "~> 2.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | gem "rake", "~> 12.0" 9 | gem "warning" 10 | 11 | gem "rails", "~> 6.0.0" 12 | gem "turbo-rails", "~> 1.4" 13 | gem "sqlite3", "~> 1.4", platforms: :mri 14 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 15 | 16 | gem "capybara" 17 | 18 | if RUBY_VERSION >= "3.1.0" 19 | # mail gem dependencies on Ruby 3.1+ 20 | gem "net-smtp" 21 | gem "net-imap" 22 | gem "net-pop" 23 | 24 | # rake gem dependency on Ruby 3.1+ 25 | gem "matrix" 26 | end 27 | 28 | if RUBY_VERSION >= "3.4.0" 29 | gem "mutex_m" 30 | gem "drb" 31 | end 32 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-6.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "sequel-activerecord_connection", "~> 2.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | gem "rake", "~> 12.0" 9 | gem "warning" 10 | 11 | gem "rails", "~> 6.1.0" 12 | gem "turbo-rails", "~> 1.4" 13 | gem "sqlite3", "~> 1.4", platforms: :mri 14 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 15 | 16 | gem "capybara" 17 | 18 | if RUBY_VERSION >= "3.1.0" 19 | # mail gem dependencies on Ruby 3.1+ 20 | gem "net-smtp" 21 | gem "net-imap" 22 | gem "net-pop" 23 | 24 | # rake gem dependency on Ruby 3.1+ 25 | gem "matrix" 26 | end 27 | 28 | if RUBY_VERSION >= "3.4.0" 29 | gem "mutex_m" 30 | gem "drb" 31 | end 32 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-7.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "sequel-activerecord_connection", "~> 2.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | gem "rake", "~> 12.0" 9 | gem "warning" 10 | 11 | gem "rails", "~> 7.0.0" 12 | gem "turbo-rails", "~> 1.4" 13 | gem "sqlite3", "~> 1.4", platforms: :mri 14 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 15 | 16 | gem "capybara" 17 | 18 | if RUBY_VERSION >= "3.1.0" 19 | # mail gem dependencies on Ruby 3.1+ 20 | gem "net-smtp" 21 | gem "net-imap" 22 | gem "net-pop" 23 | 24 | # rake gem dependency on Ruby 3.1+ 25 | gem "matrix" 26 | end 27 | 28 | if RUBY_VERSION >= "3.4.0" 29 | gem "mutex_m" 30 | gem "drb" 31 | end 32 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-7.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "sequel-activerecord_connection", "~> 2.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | gem "rake", "~> 12.0" 9 | gem "warning" 10 | 11 | gem "rails", "~> 7.1.0" 12 | gem "turbo-rails", "~> 1.4" 13 | gem "sqlite3", "~> 1.4", platforms: :mri 14 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 15 | 16 | gem "capybara" 17 | 18 | if RUBY_VERSION >= "3.1.0" 19 | # mail gem dependencies on Ruby 3.1+ 20 | gem "net-smtp" 21 | gem "net-imap" 22 | gem "net-pop" 23 | 24 | # rake gem dependency on Ruby 3.1+ 25 | gem "matrix" 26 | end 27 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-7.2: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "sequel-activerecord_connection", "~> 2.0" 6 | 7 | gem "rake", "~> 12.0" 8 | gem "warning" 9 | 10 | gem "rails", "~> 7.2.0" 11 | gem "sqlite3", "~> 1.4", platforms: :mri 12 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 13 | 14 | gem "capybara" 15 | 16 | if RUBY_VERSION >= "3.1.0" 17 | # mail gem dependencies on Ruby 3.1+ 18 | gem "net-smtp" 19 | gem "net-imap" 20 | gem "net-pop" 21 | 22 | # rake gem dependency on Ruby 3.1+ 23 | gem "matrix" 24 | end 25 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-8.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "sequel-activerecord_connection", "~> 2.0" 6 | 7 | gem "rake", "~> 12.0" 8 | gem "warning" 9 | 10 | gem "rails", "~> 8.0.0" 11 | gem "sqlite3", "~> 2.0", platforms: :mri 12 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 13 | 14 | gem "capybara" 15 | 16 | if RUBY_VERSION >= "3.1.0" 17 | # mail gem dependencies on Ruby 3.1+ 18 | gem "net-smtp" 19 | gem "net-imap" 20 | gem "net-pop" 21 | 22 | # rake gem dependency on Ruby 3.1+ 23 | gem "matrix" 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/email_auth.erb: -------------------------------------------------------------------------------- 1 | def email_auth(name, account_id, key) 2 | @rodauth = rodauth(name, account_id) { @email_auth_key_value = key } 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.email_auth_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/otp_disabled.erb: -------------------------------------------------------------------------------- 1 | def otp_disabled(name, account_id) 2 | @rodauth = rodauth(name, account_id) 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.otp_disabled_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/otp_locked_out.erb: -------------------------------------------------------------------------------- 1 | def otp_locked_out(name, account_id) 2 | @rodauth = rodauth(name, account_id) 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.otp_locked_out_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/otp_setup.erb: -------------------------------------------------------------------------------- 1 | def otp_setup(name, account_id) 2 | @rodauth = rodauth(name, account_id) 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.otp_setup_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/otp_unlock_failed.erb: -------------------------------------------------------------------------------- 1 | def otp_unlock_failed(name, account_id) 2 | @rodauth = rodauth(name, account_id) 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.otp_unlock_failed_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/otp_unlocked.erb: -------------------------------------------------------------------------------- 1 | def otp_unlocked(name, account_id) 2 | @rodauth = rodauth(name, account_id) 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.otp_unlocked_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/password_changed.erb: -------------------------------------------------------------------------------- 1 | def password_changed(name, account_id) 2 | @rodauth = rodauth(name, account_id) 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.password_changed_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/reset_password.erb: -------------------------------------------------------------------------------- 1 | def reset_password(name, account_id, key) 2 | @rodauth = rodauth(name, account_id) { @reset_password_key_value = key } 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.reset_password_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/reset_password_notify.erb: -------------------------------------------------------------------------------- 1 | def reset_password_notify(name, account_id) 2 | @rodauth = rodauth(name, account_id) 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.reset_password_notify_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/unlock_account.erb: -------------------------------------------------------------------------------- 1 | def unlock_account(name, account_id, key) 2 | @rodauth = rodauth(name, account_id) { @unlock_account_key_value = key } 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.unlock_account_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/verify_account.erb: -------------------------------------------------------------------------------- 1 | def verify_account(name, account_id, key) 2 | @rodauth = rodauth(name, account_id) { @verify_account_key_value = key } 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.verify_account_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/verify_login_change.erb: -------------------------------------------------------------------------------- 1 | def verify_login_change(name, account_id, key) 2 | @rodauth = rodauth(name, account_id) { @verify_login_change_key_value = key } 3 | @account = @rodauth.rails_account 4 | @new_email = @account.login_change_key.login 5 | 6 | mail to: @new_email, subject: @rodauth.email_subject_prefix + @rodauth.verify_login_change_email_subject 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/webauthn_authenticator_added.erb: -------------------------------------------------------------------------------- 1 | def webauthn_authenticator_added(name, account_id) 2 | @rodauth = rodauth(name, account_id) 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.webauthn_authenticator_added_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/mailer/webauthn_authenticator_removed.erb: -------------------------------------------------------------------------------- 1 | def webauthn_authenticator_removed(name, account_id) 2 | @rodauth = rodauth(name, account_id) 3 | @account = @rodauth.rails_account 4 | 5 | mail subject: @rodauth.email_subject_prefix + @rodauth.webauthn_authenticator_removed_email_subject 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/account_expiration.erb: -------------------------------------------------------------------------------- 1 | # Used by the account expiration feature 2 | create_table :<%= table_prefix %>_activity_times, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.datetime :last_activity_at, null: false 6 | t.datetime :last_login_at, null: false 7 | t.datetime :expired_at 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/active_sessions.erb: -------------------------------------------------------------------------------- 1 | # Used by the active sessions feature 2 | create_table :<%= table_prefix %>_active_session_keys, primary_key: [:<%= table_prefix %>_id, :session_id] do |t| 3 | t.references :<%= table_prefix %>, 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 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/audit_logging.erb: -------------------------------------------------------------------------------- 1 | # Used by the audit logging feature 2 | create_table :<%= table_prefix %>_authentication_audit_logs<%= primary_key_type %> do |t| 3 | t.references :<%= table_prefix %>, 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", "trilogy" -%> 10 | t.json :metadata 11 | <% else -%> 12 | t.string :metadata 13 | <% end -%> 14 | t.index [:<%= table_prefix %>_id, :at] 15 | t.index :at 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/base.erb: -------------------------------------------------------------------------------- 1 | <% if activerecord_adapter == "postgresql" -%> 2 | enable_extension "citext" 3 | 4 | <% end -%> 5 | create_table :<%= table_prefix.pluralize %><%= primary_key_type %> do |t| 6 | t.integer :status, null: false, default: 1 7 | <% case activerecord_adapter -%> 8 | <% when "postgresql" -%> 9 | t.citext :email, null: false 10 | t.check_constraint "email ~ '^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$'", name: "valid_email" 11 | <% else -%> 12 | t.string :email, null: false 13 | <% end -%> 14 | <% case activerecord_adapter -%> 15 | <% when "postgresql", "sqlite3" -%> 16 | t.index :email, unique: true, where: "status IN (1, 2)" 17 | <% else -%> 18 | t.index :email, unique: true 19 | <% end -%> 20 | t.string :password_hash 21 | end 22 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/disallow_password_reuse.erb: -------------------------------------------------------------------------------- 1 | # Used by the disallow password reuse feature 2 | create_table :<%= table_prefix %>_previous_password_hashes do |t| 3 | t.references :<%= table_prefix %>, foreign_key: true<%= primary_key_type(:type) %> 4 | t.string :password_hash, null: false 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/email_auth.erb: -------------------------------------------------------------------------------- 1 | # Used by the email auth feature 2 | create_table :<%= table_prefix %>_email_auth_keys, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :key, null: false 6 | t.datetime :deadline, null: false 7 | t.datetime :email_last_sent, null: false, default: -> { "<%= current_timestamp %>" } 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/jwt_refresh.erb: -------------------------------------------------------------------------------- 1 | # Used by the jwt refresh feature 2 | create_table :<%= table_prefix %>_jwt_refresh_keys<%= primary_key_type %> do |t| 3 | t.references :<%= table_prefix %>, foreign_key: true, null: false<%= primary_key_type(:type) %> 4 | t.string :key, null: false 5 | t.datetime :deadline, null: false 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/lockout.erb: -------------------------------------------------------------------------------- 1 | # Used by the lockout feature 2 | create_table :<%= table_prefix %>_login_failures, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.integer :number, null: false, default: 1 6 | end 7 | create_table :<%= table_prefix %>_lockouts, id: false do |t| 8 | t.<%= primary_key_type(nil) %> :id, primary_key: true 9 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 10 | t.string :key, null: false 11 | t.datetime :deadline, null: false 12 | t.datetime :email_last_sent 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/otp.erb: -------------------------------------------------------------------------------- 1 | # Used by the otp feature 2 | create_table :<%= table_prefix %>_otp_keys, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :key, null: false 6 | t.integer :num_failures, null: false, default: 0 7 | t.datetime :last_use, null: false, default: -> { "<%= current_timestamp %>" } 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/otp_unlock.erb: -------------------------------------------------------------------------------- 1 | # Used by the otp_unlock feature 2 | create_table :<%= table_prefix %>_otp_unlocks, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.integer :num_successes, null: false, default: 1 6 | t.datetime :next_auth_attempt_after, null: false, default: -> { "<%= current_timestamp %>" } 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/password_expiration.erb: -------------------------------------------------------------------------------- 1 | # Used by the password expiration feature 2 | create_table :<%= table_prefix %>_password_change_times, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.datetime :changed_at, null: false, default: -> { "<%= current_timestamp %>" } 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/recovery_codes.erb: -------------------------------------------------------------------------------- 1 | # Used by the recovery codes feature 2 | create_table :<%= table_prefix %>_recovery_codes, primary_key: [:id, :code] do |t| 3 | t.<%= primary_key_type(nil) %> :id 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :code 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/remember.erb: -------------------------------------------------------------------------------- 1 | # Used by the remember me feature 2 | create_table :<%= table_prefix %>_remember_keys, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :key, null: false 6 | t.datetime :deadline, null: false 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/reset_password.erb: -------------------------------------------------------------------------------- 1 | # Used by the password reset feature 2 | create_table :<%= table_prefix %>_password_reset_keys, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :key, null: false 6 | t.datetime :deadline, null: false 7 | t.datetime :email_last_sent, null: false, default: -> { "<%= current_timestamp %>" } 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/single_session.erb: -------------------------------------------------------------------------------- 1 | # Used by the single session feature 2 | create_table :<%= table_prefix %>_session_keys, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :key, null: false 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/sms_codes.erb: -------------------------------------------------------------------------------- 1 | # Used by the sms codes feature 2 | create_table :<%= table_prefix %>_sms_codes, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :phone_number, null: false 6 | t.integer :num_failures 7 | t.string :code 8 | t.datetime :code_issued_at, null: false, default: -> { "<%= current_timestamp %>" } 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/verify_account.erb: -------------------------------------------------------------------------------- 1 | # Used by the account verification feature 2 | create_table :<%= table_prefix %>_verification_keys, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :key, null: false 6 | t.datetime :requested_at, null: false, default: -> { "<%= current_timestamp %>" } 7 | t.datetime :email_last_sent, null: false, default: -> { "<%= current_timestamp %>" } 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/verify_login_change.erb: -------------------------------------------------------------------------------- 1 | # Used by the verify login change feature 2 | create_table :<%= table_prefix %>_login_change_keys, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :key, null: false 6 | t.string :login, null: false 7 | t.datetime :deadline, null: false 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/active_record/webauthn.erb: -------------------------------------------------------------------------------- 1 | # Used by the webauthn feature 2 | create_table :<%= table_prefix %>_webauthn_user_ids, id: false do |t| 3 | t.<%= primary_key_type(nil) %> :id, primary_key: true 4 | t.foreign_key :<%= table_prefix.pluralize %>, column: :id 5 | t.string :webauthn_id, null: false 6 | end 7 | create_table :<%= table_prefix %>_webauthn_keys, primary_key: [:<%= table_prefix %>_id, :webauthn_id] do |t| 8 | t.references :<%= table_prefix %>, foreign_key: true<%= primary_key_type(:type) %> 9 | t.string :webauthn_id 10 | t.string :public_key, null: false 11 | t.integer :sign_count, null: false 12 | t.datetime :last_use, null: false, default: -> { "<%= current_timestamp %>" } 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/account_expiration.erb: -------------------------------------------------------------------------------- 1 | # Used by the account expiration feature 2 | create_table :<%= table_prefix %>_activity_times do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | DateTime :last_activity_at, null: false 5 | DateTime :last_login_at, null: false 6 | DateTime :expired_at 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/active_sessions.erb: -------------------------------------------------------------------------------- 1 | # Used by the active sessions feature 2 | create_table :<%= table_prefix %>_active_session_keys do 3 | foreign_key :<%= table_prefix %>_id, :<%= table_prefix.pluralize %>, type: :Bignum 4 | String :session_id 5 | Time :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP 6 | Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP 7 | primary_key [:<%= table_prefix %>_id, :session_id] 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/audit_logging.erb: -------------------------------------------------------------------------------- 1 | # Used by the audit logging feature 2 | create_table :<%= table_prefix %>_authentication_audit_logs do 3 | primary_key :id, type: :Bignum 4 | foreign_key :<%= table_prefix %>_id, :<%= table_prefix.pluralize %>, null: false, type: :Bignum 5 | DateTime :at, null: false, default: Sequel::CURRENT_TIMESTAMP 6 | String :message, null: false 7 | <% case db.database_type -%> 8 | <% when :postgres -%> 9 | jsonb :metadata 10 | <% when :sqlite, :mysql -%> 11 | json :metadata 12 | <% else -%> 13 | String :metadata 14 | <% end -%> 15 | index [:<%= table_prefix %>_id, :at] 16 | index :at 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/base.erb: -------------------------------------------------------------------------------- 1 | <% if db.database_type == :postgres -%> 2 | begin 3 | run "CREATE EXTENSION IF NOT EXISTS citext" 4 | rescue NoMethodError # migration is being reverted 5 | end 6 | 7 | <% end -%> 8 | create_table :<%= table_prefix.pluralize %> do 9 | primary_key :id, type: :Bignum 10 | <% if db.database_type == :postgres -%> 11 | citext :email, null: false 12 | constraint :valid_email, email: /^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$/ 13 | <% else -%> 14 | String :email, null: false 15 | <% end -%> 16 | Integer :status, null: false, default: 1 17 | <% if db.supports_partial_indexes? -%> 18 | index :email, unique: true, where: { status: [1, 2] } 19 | <% else -%> 20 | index :email, unique: true 21 | <% end -%> 22 | String :password_hash 23 | end 24 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/disallow_password_reuse.erb: -------------------------------------------------------------------------------- 1 | # Used by the disallow password reuse feature 2 | create_table :<%= table_prefix %>_previous_password_hashes do 3 | primary_key :id, type: :Bignum 4 | foreign_key :<%= table_prefix %>_id, :<%= table_prefix.pluralize %>, type: :Bignum 5 | String :password_hash, null: false 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/email_auth.erb: -------------------------------------------------------------------------------- 1 | # Used by the email auth feature 2 | create_table :<%= table_prefix %>_email_auth_keys do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | String :key, null: false 5 | DateTime :deadline, null: false 6 | DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/jwt_refresh.erb: -------------------------------------------------------------------------------- 1 | # Used by the jwt refresh feature 2 | create_table :<%= table_prefix %>_jwt_refresh_keys do 3 | primary_key :id, type: :Bignum 4 | foreign_key :<%= table_prefix %>_id, :<%= table_prefix.pluralize %>, null: false, type: :Bignum 5 | String :key, null: false 6 | DateTime :deadline, null: false 7 | index :<%= table_prefix %>_id 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/lockout.erb: -------------------------------------------------------------------------------- 1 | # Used by the lockout feature 2 | create_table :<%= table_prefix %>_login_failures do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | Integer :number, null: false, default: 1 5 | end 6 | create_table :<%= table_prefix %>_lockouts do 7 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 8 | String :key, null: false 9 | DateTime :deadline, null: false 10 | DateTime :email_last_sent 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/otp.erb: -------------------------------------------------------------------------------- 1 | # Used by the otp feature 2 | create_table :<%= table_prefix %>_otp_keys do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | String :key, null: false 5 | Integer :num_failures, null: false, default: 0 6 | Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/otp_unlock.erb: -------------------------------------------------------------------------------- 1 | # Used by the otp_unlock feature 2 | create_table :<%= table_prefix %>_otp_unlocks do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | Integer :num_successes, null: false, default: 1 5 | Time :next_auth_attempt_after, null: false, default: Sequel::CURRENT_TIMESTAMP 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/password_expiration.erb: -------------------------------------------------------------------------------- 1 | # Used by the password expiration feature 2 | create_table :<%= table_prefix %>_password_change_times do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | DateTime :changed_at, null: false, default: Sequel::CURRENT_TIMESTAMP 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/recovery_codes.erb: -------------------------------------------------------------------------------- 1 | # Used by the recovery codes feature 2 | create_table :<%= table_prefix %>_recovery_codes do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, type: :Bignum 4 | String :code 5 | primary_key [:id, :code] 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/remember.erb: -------------------------------------------------------------------------------- 1 | # Used by the remember me feature 2 | create_table :<%= table_prefix %>_remember_keys do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | String :key, null: false 5 | DateTime :deadline, null: false 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/reset_password.erb: -------------------------------------------------------------------------------- 1 | # Used by the password reset feature 2 | create_table :<%= table_prefix %>_password_reset_keys do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | String :key, null: false 5 | DateTime :deadline, null: false 6 | DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/single_session.erb: -------------------------------------------------------------------------------- 1 | # Used by the single session feature 2 | create_table :<%= table_prefix %>_session_keys do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | String :key, null: false 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/sms_codes.erb: -------------------------------------------------------------------------------- 1 | # Used by the sms codes feature 2 | create_table :<%= table_prefix %>_sms_codes do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | String :phone_number, null: false 5 | Integer :num_failures 6 | String :code 7 | DateTime :code_issued_at, null: false, default: Sequel::CURRENT_TIMESTAMP 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/verify_account.erb: -------------------------------------------------------------------------------- 1 | # Used by the account verification feature 2 | create_table :<%= table_prefix %>_verification_keys do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | String :key, null: false 5 | DateTime :requested_at, null: false, default: Sequel::CURRENT_TIMESTAMP 6 | DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/verify_login_change.erb: -------------------------------------------------------------------------------- 1 | # Used by the verify login change feature 2 | create_table :<%= table_prefix %>_login_change_keys do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | String :key, null: false 5 | String :login, null: false 6 | DateTime :deadline, null: false 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/migration/sequel/webauthn.erb: -------------------------------------------------------------------------------- 1 | # Used by the webauthn feature 2 | create_table :<%= table_prefix %>_webauthn_user_ids do 3 | foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum 4 | String :webauthn_id, null: false 5 | end 6 | create_table :<%= table_prefix %>_webauthn_keys do 7 | foreign_key :<%= table_prefix %>_id, :<%= table_prefix.pluralize %>, type: :Bignum 8 | String :webauthn_id 9 | String :public_key, null: false 10 | Integer :sign_count, null: false 11 | Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP 12 | primary_key [:<%= table_prefix %>_id, :webauthn_id] 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/INSTRUCTIONS: -------------------------------------------------------------------------------- 1 | =============================================================================== 2 | 3 | * Ensure you have defined a root path in your config/routes.rb. For example: 4 | 5 | root to: "pages#home" 6 | 7 | * Ensure you're displaying flash messages in your layout template. For example: 8 | 9 | <% if notice %> 10 |
<%= notice %>
11 | <% end %> 12 | <% if alert %> 13 |
<%= alert %>
14 | <% end %> 15 | 16 | * Titles for Rodauth pages are available via @page_title instance variable 17 | by default, you can use it in your layout file: 18 | 19 | <%= @page_title || "Default title" %> 20 | 21 | * You can copy Rodauth views into your app by running: 22 | 23 | rails g rodauth:views # default bootstrap views 24 | 25 | rails g rodauth:views --css=tailwind # tailwind views (requires @tailwindcss/forms plugin) 26 | 27 | * You can copy email templates and generate mailer integration by running: 28 | 29 | rails g rodauth:mailer 30 | 31 | =============================================================================== 32 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb.tt: -------------------------------------------------------------------------------- 1 | class RodauthController < ApplicationController 2 | # Used by Rodauth for rendering views, CSRF protection, running any 3 | # registered action callbacks and rescue handlers, instrumentation etc. 4 | 5 | # Controller callbacks and rescue handlers will run around Rodauth endpoints. 6 | # before_action :verify_captcha, only: :login, if: -> { request.post? } 7 | # rescue_from("SomeError") { |exception| ... } 8 | 9 | # Layout can be changed for all Rodauth pages or only certain pages. 10 | # layout "authentication" 11 | # layout -> do 12 | # case rodauth.current_route 13 | # when :login, :create_account, :verify_account, :verify_account_resend, 14 | # :reset_password, :reset_password_request 15 | # "authentication" 16 | # else 17 | # "application" 18 | # end 19 | # end 20 | end 21 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb.tt: -------------------------------------------------------------------------------- 1 | class RodauthMailer < ApplicationMailer 2 | default to: -> { @rodauth.email_to }, from: -> { @rodauth.email_from } 3 | 4 | <%= mailer_content -%> 5 | 6 | private 7 | 8 | # Default URL options are inherited from Action Mailer, but you can override them 9 | # ad-hoc by modifying the `rodauth.rails_url_options` hash. 10 | def rodauth(name, account_id, &block) 11 | instance = RodauthApp.rodauth(name).allocate 12 | instance.account_from_id(account_id) 13 | instance.instance_eval(&block) if block 14 | instance 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/misc/rodauth_app.rb.tt: -------------------------------------------------------------------------------- 1 | class RodauthApp < Rodauth::Rails::App 2 | # primary configuration 3 | configure RodauthMain 4 | 5 | # secondary configuration 6 | # configure RodauthAdmin, :admin 7 | 8 | route do |r| 9 | <% unless jwt? -%> 10 | rodauth.load_memory # autologin remembered users 11 | 12 | <% end -%> 13 | r.rodauth # route rodauth requests 14 | 15 | # ==> Authenticating requests 16 | # Call `rodauth.require_account` for requests that you want to 17 | # require authentication for. For example: 18 | # 19 | # # authenticate /dashboard/* and /account/* requests 20 | # if r.path.start_with?("/dashboard") || r.path.start_with?("/account") 21 | # rodauth.require_account 22 | # end 23 | 24 | # ==> Secondary configurations 25 | # r.rodauth(:admin) # route admin rodauth requests 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/models/account.rb.tt: -------------------------------------------------------------------------------- 1 | <% if activerecord? -%> 2 | class <%= table_prefix.camelize %> < ApplicationRecord 3 | include Rodauth::Rails.model 4 | <% if ActiveRecord.version >= Gem::Version.new("7.0") -%> 5 | enum :status, { unverified: 1, verified: 2, closed: 3 } 6 | <% else -%> 7 | enum status: { unverified: 1, verified: 2, closed: 3 } 8 | <% end -%> 9 | end 10 | <% else -%> 11 | class <%= table_prefix.camelize %> < Sequel::Model 12 | include Rodauth::Rails.model 13 | plugin :enum 14 | enum :status, unverified: 1, verified: 2, closed: 3 15 | end 16 | <% end -%> 17 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/_email_auth_request_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.email_auth_request_path, method: :post, data: { turbo: false } do |form| %> 2 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 3 | 4 |
5 | <%= form.submit rodauth.email_auth_request_button, class: "btn btn-primary" %> 6 |
7 | <% end %> 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/_login_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.login_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.skip_login_field_on_login? %> 3 |
4 | <%= form.label "login", rodauth.login_label, class: "form-label" %> 5 | <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", readonly: true, class: "form-control-plaintext" %> 6 |
7 | <% else %> 8 |
9 | <%= form.label "login", rodauth.login_label, class: "form-label" %> 10 | <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", autocomplete: rodauth.login_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.login_param)}", aria: ({ invalid: true, describedby: "login_error_message" } if rodauth.field_error(rodauth.login_param)) %> 11 | <%= content_tag(:span, rodauth.field_error(rodauth.login_param), class: "invalid-feedback", id: "login_error_message") if rodauth.field_error(rodauth.login_param) %> 12 |
13 | <% end %> 14 | 15 | <% unless rodauth.skip_password_field_on_login? %> 16 |
17 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 18 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 19 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 20 |
21 | <% end %> 22 | 23 |
24 | <%= form.submit rodauth.login_button, class: "btn btn-primary" %> 25 |
26 | <% end %> 27 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/_login_form_footer.html.erb: -------------------------------------------------------------------------------- 1 | <% unless rodauth.login_form_footer_links.empty? %> 2 | <%== rodauth.login_form_footer_links_heading %> 3 | 4 | 9 | <% end %> 10 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/add_recovery_codes.html.erb: -------------------------------------------------------------------------------- 1 |
<%= rodauth.recovery_codes.map { |s| h(s) }.join("\n\n") %>
2 | 3 | <% if rodauth.can_add_recovery_codes? %> 4 | <%== rodauth.add_recovery_codes_heading %> 5 | <%= render template: "rodauth/recovery_codes", layout: false %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/change_login.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.change_login_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.label "login", rodauth.login_label, class: "form-label" %> 4 | <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", autocomplete: "email", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.login_param)}", aria: ({ invalid: true, describedby: "login_error_message" } if rodauth.field_error(rodauth.login_param)) %> 5 | <%= content_tag(:span, rodauth.field_error(rodauth.login_param), class: "invalid-feedback", id: "login_error_message") if rodauth.field_error(rodauth.login_param) %> 6 |
7 | 8 | <% if rodauth.require_login_confirmation? %> 9 |
10 | <%= form.label "login-confirm", rodauth.login_confirm_label, class: "form-label" %> 11 | <%= form.email_field rodauth.login_confirm_param, value: params[rodauth.login_confirm_param], id: "login-confirm", autocomplete: "email", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.login_confirm_param)}", aria: ({ invalid: true, describedby: "login-confirm_error_message" } if rodauth.field_error(rodauth.login_confirm_param)) %> 12 | <%= content_tag(:span, rodauth.field_error(rodauth.login_confirm_param), class: "invalid-feedback", id: "login-confirm_error_message") if rodauth.field_error(rodauth.login_confirm_param) %> 13 |
14 | <% end %> 15 | 16 | <% if rodauth.change_login_requires_password? %> 17 |
18 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 19 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 20 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 21 |
22 | <% end %> 23 | 24 |
25 | <%= form.submit rodauth.change_login_button, class: "btn btn-primary" %> 26 |
27 | <% end %> 28 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/change_password.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.change_password_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.change_password_requires_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <%= form.label "new-password", rodauth.new_password_label, class: "form-label" %> 12 | <%= form.password_field rodauth.new_password_param, value: "", id: "new-password", autocomplete: "new-password", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.new_password_param)}", aria: ({ invalid: true, describedby: "new-password_error_message" } if rodauth.field_error(rodauth.new_password_param)) %> 13 | <%= content_tag(:span, rodauth.field_error(rodauth.new_password_param), class: "invalid-feedback", id: "new-password_error_message") if rodauth.field_error(rodauth.new_password_param) %> 14 |
15 | 16 | <% if rodauth.require_password_confirmation? %> 17 |
18 | <%= form.label "password-confirm", rodauth.password_confirm_label, class: "form-label" %> 19 | <%= form.password_field rodauth.password_confirm_param, value: "", id: "password-confirm", autocomplete: "new-password", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_confirm_param)}", aria: ({ invalid: true, describedby: "password-confirm_error_message" } if rodauth.field_error(rodauth.password_confirm_param)) %> 20 | <%= content_tag(:span, rodauth.field_error(rodauth.password_confirm_param), class: "invalid-feedback", id: "password-confirm_error_message") if rodauth.field_error(rodauth.password_confirm_param) %> 21 |
22 | <% end %> 23 | 24 |
25 | <%= form.submit rodauth.change_password_button, class: "btn btn-primary" %> 26 |
27 | <% end %> 28 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/close_account.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.close_account_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.close_account_requires_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <%= form.submit rodauth.close_account_button, class: "btn btn-danger" %> 12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/confirm_password.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.confirm_password_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 4 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 5 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 6 |
7 | 8 |
9 | <%= form.submit rodauth.confirm_password_button, class: "btn btn-primary" %> 10 |
11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/email_auth.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.email_auth_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.submit rodauth.login_button, class: "btn btn-primary" %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/login.html.erb: -------------------------------------------------------------------------------- 1 | <%== rodauth.login_form_header %> 2 | <%= render "login_form" %> 3 | <%== rodauth.login_form_footer %> 4 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/logout.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.logout_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.features.include?(:active_sessions) %> 3 |
4 |
5 | <%= form.check_box rodauth.global_logout_param, id: "global-logout", class: "form-check-input", include_hidden: false %> 6 | <%= form.label "global-logout", rodauth.global_logout_label, class: "form-check-label" %> 7 |
8 |
9 | <% end %> 10 | 11 |
12 | <%= form.submit rodauth.logout_button, class: "btn btn-warning" %> 13 |
14 | <% end %> 15 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/multi_phase_login.html.erb: -------------------------------------------------------------------------------- 1 | <%== rodauth.login_form_header %> 2 | <%== rodauth.render_multi_phase_login_forms %> 3 | <%== rodauth.login_form_footer %> 4 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/otp_auth.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.otp_auth_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.label "otp-auth-code", rodauth.otp_auth_label, class: "form-label" %> 4 |
5 |
6 | <%= form.text_field rodauth.otp_auth_param, value: "", id: "otp-auth-code", autocomplete: "off", inputmode: "numeric", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.otp_auth_param)}", aria: ({ invalid: true, describedby: "otp-auth-code_error_message" } if rodauth.field_error(rodauth.otp_auth_param)) %> 7 | <%= content_tag(:span, rodauth.field_error(rodauth.otp_auth_param), class: "invalid-feedback", id: "otp-auth-code_error_message") if rodauth.field_error(rodauth.otp_auth_param) %> 8 |
9 |
10 |
11 | 12 |
13 | <%= form.submit rodauth.otp_auth_button, class: "btn btn-primary" %> 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/otp_disable.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.otp_disable_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <%= form.submit rodauth.otp_disable_button, class: "btn btn-warning" %> 12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.otp_setup_path, method: :post, data: { turbo: false } do |form| %> 2 | <%= form.hidden_field rodauth.otp_setup_param, value: rodauth.otp_user_key, id: "otp-key" %> 3 | <%= form.hidden_field rodauth.otp_setup_raw_param, value: rodauth.otp_key, id: "otp-hmac-secret" if rodauth.otp_keys_use_hmac? %> 4 | 5 |
6 |

<%= rodauth.otp_secret_label %>: <%= rodauth.otp_user_key %>

7 |

<%= rodauth.otp_provisioning_uri_label %>: <%= rodauth.otp_provisioning_uri %>

8 |
9 | 10 |
11 |
12 |
13 |

<%== rodauth.otp_qr_code %>

14 |
15 |
16 | 17 |
18 | <% if rodauth.two_factor_modifications_require_password? %> 19 |
20 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 21 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 22 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 23 |
24 | <% end %> 25 | 26 |
27 | <%= form.label "otp-auth-code", rodauth.otp_auth_label, class: "form-label" %> 28 |
29 |
30 | <%= form.text_field rodauth.otp_auth_param, value: "", id: "otp-auth-code", autocomplete: "off", inputmode: "numeric", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.otp_auth_param)}", aria: ({ invalid: true, describedby: "otp-auth-code_error_message" } if rodauth.field_error(rodauth.otp_auth_param)) %> 31 | <%= content_tag(:span, rodauth.field_error(rodauth.otp_auth_param), class: "invalid-feedback", id: "otp-auth-code_error_message") if rodauth.field_error(rodauth.otp_auth_param) %> 32 |
33 |
34 |
35 | 36 |
37 | <%= form.submit rodauth.otp_setup_button, class: "btn btn-primary" %> 38 |
39 |
40 |
41 | <% end %> 42 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/otp_unlock.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.otp_unlock_path, method: :post, data: { turbo: false } do |form| %> 2 |

<%= rodauth.otp_unlock_consecutive_successes_label %>: <%= rodauth.otp_unlock_num_successes %>

3 |

<%= rodauth.otp_unlock_required_consecutive_successes_label %>: <%= rodauth.otp_unlock_auths_required %>

4 |

<%= rodauth.otp_unlock_next_auth_deadline_label %>: <%= rodauth.otp_unlock_deadline.strftime(rodauth.strftime_format) %>

5 | 6 |
7 | <%= form.label "otp-auth-code", rodauth.otp_auth_label, class: "form-label" %> 8 |
9 |
10 | <%= form.text_field rodauth.otp_auth_param, value: "", id: "otp-auth-code", autocomplete: "off", inputmode: "numeric", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.otp_auth_param)}", aria: ({ invalid: true, describedby: "otp-auth-code_error_message" } if rodauth.field_error(rodauth.otp_auth_param)) %> 11 | <%= content_tag(:span, rodauth.field_error(rodauth.otp_auth_param), class: "invalid-feedback", id: "otp-auth-code_error_message") if rodauth.field_error(rodauth.otp_auth_param) %> 12 |
13 |
14 |
15 | 16 |
17 | <%= form.submit rodauth.otp_unlock_button, class: "btn btn-primary" %> 18 |
19 | <% end %> 20 | 21 | <%== rodauth.otp_unlock_form_footer %> 22 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/otp_unlock_not_available.html.erb: -------------------------------------------------------------------------------- 1 |

<%= rodauth.otp_unlock_consecutive_successes_label %>: <%= rodauth.otp_unlock_num_successes %>

2 |

<%= rodauth.otp_unlock_required_consecutive_successes_label %>: <%= rodauth.otp_unlock_auths_required %>

3 |

<%= rodauth.otp_unlock_next_auth_attempt_label %>: <%= rodauth.otp_unlock_next_auth_attempt_after.strftime(rodauth.strftime_format) %>

4 |

<%= rodauth.otp_unlock_next_auth_attempt_refresh_label %>

5 | <%== rodauth.otp_unlock_refresh_tag %> 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/recovery_auth.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.recovery_auth_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.label "recovery-code", rodauth.recovery_codes_label, class: "form-label" %> 4 | <%= form.text_field rodauth.recovery_codes_param, value: "", id: "recovery-code", autocomplete: "off", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.recovery_codes_param)}", aria: ({ invalid: true, describedby: "recovery-code_error_message" } if rodauth.field_error(rodauth.recovery_codes_param)) %> 5 | <%= content_tag(:span, rodauth.field_error(rodauth.recovery_codes_param), class: "invalid-feedback", id: "recovery-code_error_message") if rodauth.field_error(rodauth.recovery_codes_param) %> 6 |
7 | 8 |
9 | <%= form.submit rodauth.recovery_auth_button, class: "btn btn-primary" %> 10 |
11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/recovery_codes.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.recovery_codes_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <%= form.submit rodauth.recovery_codes_button || rodauth.view_recovery_codes_button, name: (rodauth.add_recovery_codes_param if rodauth.recovery_codes_button), class: "btn btn-primary" %> 12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.remember_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 |
4 | <%= form.radio_button rodauth.remember_param, rodauth.remember_remember_param_value, id: "remember-remember", class: "form-check-input" %> 5 | <%= form.label "remember-remember", rodauth.remember_remember_label, class: "form-check-label" %> 6 |
7 | 8 |
9 | <%= form.radio_button rodauth.remember_param, rodauth.remember_forget_param_value, id: "remember-forget", class: "form-check-input" %> 10 | <%= form.label "remember-forget", rodauth.remember_forget_label, class: "form-check-label" %> 11 |
12 | 13 |
14 | <%= form.radio_button rodauth.remember_param, rodauth.remember_disable_param_value, id: "remember-disable", class: "form-check-input" %> 15 | <%= form.label "remember-disable", rodauth.remember_disable_label, class: "form-check-label" %> 16 |
17 |
18 | 19 |
20 | <%= form.submit rodauth.remember_button, class: "btn btn-primary" %> 21 |
22 | <% end %> 23 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/reset_password.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.reset_password_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 4 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 5 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 6 |
7 | 8 | <% if rodauth.require_password_confirmation? %> 9 |
10 | <%= form.label "password-confirm", rodauth.password_confirm_label, class: "form-label" %> 11 | <%= form.password_field rodauth.password_confirm_param, value: "", id: "password-confirm", autocomplete: "new-password", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_confirm_param)}", aria: ({ invalid: true, describedby: "password-confirm_error_message" } if rodauth.field_error(rodauth.password_confirm_param)) %> 12 | <%= content_tag(:span, rodauth.field_error(rodauth.password_confirm_param), class: "invalid-feedback", id: "password-confirm_error_message") if rodauth.field_error(rodauth.password_confirm_param) %> 13 |
14 | <% end %> 15 | 16 |
17 | <%= form.submit rodauth.reset_password_button, class: "btn btn-primary" %> 18 |
19 | <% end %> 20 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/reset_password_request.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.reset_password_request_path, method: :post, data: { turbo: false } do |form| %> 2 | <%== rodauth.reset_password_explanatory_text %> 3 | 4 | <% if params[rodauth.login_param] && !rodauth.field_error(rodauth.login_param) %> 5 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 6 | <% else %> 7 |
8 | <%= form.label "login", rodauth.login_label, class: "form-label" %> 9 | <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", autocomplete: "email", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.login_param)}", aria: ({ invalid: true, describedby: "login_error_message" } if rodauth.field_error(rodauth.login_param)) %> 10 | <%= content_tag(:span, rodauth.field_error(rodauth.login_param), class: "invalid-feedback", id: "login_error_message") if rodauth.field_error(rodauth.login_param) %> 11 |
12 | <% end %> 13 | 14 |
15 | <%= form.submit rodauth.reset_password_request_button, class: "btn btn-primary" %> 16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/sms_auth.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_auth_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.label "sms-code", rodauth.sms_code_label, class: "form-label" %> 4 |
5 |
6 | <%= form.text_field rodauth.sms_code_param, value: "", id: "sms-code", autocomplete: "one-time-code", inputmode: "numeric", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.sms_code_param)}", aria: ({ invalid: true, describedby: "sms-code_error_message" } if rodauth.field_error(rodauth.sms_code_param)) %> 7 | <%= content_tag(:span, rodauth.field_error(rodauth.sms_code_param), class: "invalid-feedback", id: "sms-code_error_message") if rodauth.field_error(rodauth.sms_code_param) %> 8 |
9 |
10 |
11 | 12 |
13 | <%= form.submit rodauth.sms_auth_button, class: "btn btn-primary" %> 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/sms_confirm.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_confirm_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.label "sms-code", rodauth.sms_code_label, class: "form-label" %> 4 |
5 |
6 | <%= form.text_field rodauth.sms_code_param, value: "", id: "sms-code", autocomplete: "one-time-code", inputmode: "numeric", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.sms_code_param)}", aria: ({ invalid: true, describedby: "sms-code_error_message" } if rodauth.field_error(rodauth.sms_code_param)) %> 7 | <%= content_tag(:span, rodauth.field_error(rodauth.sms_code_param), class: "invalid-feedback", id: "sms-code_error_message") if rodauth.field_error(rodauth.sms_code_param) %> 8 |
9 |
10 |
11 | 12 |
13 | <%= form.submit rodauth.sms_confirm_button, class: "btn btn-primary" %> 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/sms_disable.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_disable_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <%= form.submit rodauth.sms_disable_button, class: "btn btn-primary" %> 12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/sms_request.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_request_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.submit rodauth.sms_request_button, class: "btn btn-primary" %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/sms_setup.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_setup_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <%= form.label "sms-phone", rodauth.sms_phone_label, class: "form-label" %> 12 |
13 |
14 | <%= form.telephone_field rodauth.sms_phone_param, value: "", id: "sms-phone", autocomplete: "tel", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.sms_phone_param)}", aria: ({ invalid: true, describedby: "sms-phone_error_message" } if rodauth.field_error(rodauth.sms_phone_param)) %> 15 | <%= content_tag(:span, rodauth.field_error(rodauth.sms_phone_param), class: "invalid-feedback", id: "sms-phone_error_message") if rodauth.field_error(rodauth.sms_phone_param) %> 16 |
17 |
18 |
19 | 20 |
21 | <%= form.submit rodauth.sms_setup_button, class: "btn btn-primary" %> 22 |
23 | <% end %> 24 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/_email_auth_request_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.email_auth_request_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 3 | 4 | <%= form.submit rodauth.email_auth_request_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/_login_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.login_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.skip_login_field_on_login? %> 3 |
4 | <%= form.label "login", rodauth.login_label, class: "block text-sm font-semibold" %> 5 | <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", readonly: true, class: "mt-2 text-sm w-full py-2 px-0 bg-inherit border-transparent focus:ring-0 focus:border-transparent" %> 6 |
7 | <% else %> 8 |
9 | <%= form.label "login", rodauth.login_label, class: "block text-sm font-semibold" %> 10 | <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", autocomplete: rodauth.login_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.login_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "login_error_message" } if rodauth.field_error(rodauth.login_param)) %> 11 | <%= content_tag(:span, rodauth.field_error(rodauth.login_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "login_error_message") if rodauth.field_error(rodauth.login_param) %> 12 |
13 | <% end %> 14 | 15 | <% unless rodauth.skip_password_field_on_login? %> 16 |
17 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 18 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 19 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 20 |
21 | <% end %> 22 | 23 | <%= form.submit rodauth.login_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 24 | <% end %> 25 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/_login_form_footer.html.erb: -------------------------------------------------------------------------------- 1 | <% unless rodauth.login_form_footer_links.empty? %> 2 | 7 | <% end %> 8 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/add_recovery_codes.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if rodauth.recovery_codes.any? %> 3 |
4 | <% rodauth.recovery_codes.each do |code| %> 5 |
<%= code %>
6 | <% end %> 7 |
8 | <% end %> 9 | 10 | <% if rodauth.can_add_recovery_codes? %> 11 |
12 | <%== rodauth.add_recovery_codes_heading %> 13 |
14 | <%= render template: "rodauth/recovery_codes", layout: false %> 15 | <% end %> 16 |
17 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/close_account.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.close_account_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.close_account_requires_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 | <%= form.submit rodauth.close_account_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-600 dark:bg-red-400 dark:hover:bg-red-500 dark:text-gray-900 dark:focus:ring-red-400 dark:focus:ring-offset-current" %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/confirm_password.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.confirm_password_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 |
4 |

5 | <%= rodauth.confirm_password_page_title %> 6 |

7 |
8 |
9 | <%= form.label "password", rodauth.password_label, class: "text-sm text-gray-400" %> 10 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "w-full px-3 py-2 border rounded-md border-gray-400 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 focus:dark:border-emerald-400 focus:dark:bg-gray-700 invalid:border-red-500 invalid:text-red-600 focus:invalid:border-red-500 focus:invalid:ring-red-500 valid:border-green-500 #{"border rounded-md border-red-500" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 11 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "text-red-800 text-xs mt-1 ml-1", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 12 |
13 |
14 |
15 | <%= form.submit rodauth.confirm_password_button, class: "w-full px-8 py-2 font-semibold rounded-md w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-emerald-400 dark:text-gray-900" %> 16 |
17 |
18 |
19 | <% end %> 20 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/email_auth.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.email_auth_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <%= form.submit rodauth.login_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/login.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%== rodauth.login_form_header %> 3 | <%= render "login_form" %> 4 | <%== rodauth.login_form_footer %> 5 |
6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/logout.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.logout_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.features.include?(:active_sessions) %> 3 |
4 | <%= form.check_box rodauth.global_logout_param, id: "global-logout", class: "rounded dark:bg-gray-900 dark:border-gray-600 dark:checked:bg-current dark:checked:border-current dark:checked:text-emerald-400 dark:focus:ring-emerald-400 dark:focus:ring-offset-gray-900", include_hidden: false %> 5 | <%= form.label "global-logout", rodauth.global_logout_label, class: "text-sm" %> 6 |
7 | <% end %> 8 | 9 | <%= form.submit rodauth.logout_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-300 dark:bg-violet-400 dark:hover:bg-violet-500 dark:text-gray-900 dark:focus:ring-violet-400 dark:focus:ring-offset-current" %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/multi_phase_login.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%== rodauth.login_form_header %> 3 | <%== rodauth.render_multi_phase_login_forms %> 4 | <%== rodauth.login_form_footer %> 5 |
6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/otp_auth.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.otp_auth_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 | <%= form.label "otp-auth-code", rodauth.otp_auth_label, class: "block text-sm font-semibold" %> 4 | <%= form.text_field rodauth.otp_auth_param, value: "", id: "otp-auth-code", autocomplete: "off", inputmode: "numeric", required: true, class: "mt-2 text-sm w-1/2 px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.otp_auth_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "otp-auth-code_error_message" } if rodauth.field_error(rodauth.otp_auth_param)) %> 5 | <%= content_tag(:span, rodauth.field_error(rodauth.otp_auth_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "otp-auth-code_error_message") if rodauth.field_error(rodauth.otp_auth_param) %> 6 |
7 | 8 | <%= form.submit rodauth.otp_auth_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/otp_disable.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.otp_disable_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 | <%= form.submit rodauth.otp_disable_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-300 dark:bg-violet-400 dark:hover:bg-violet-500 dark:text-gray-900 dark:focus:ring-violet-400 dark:focus:ring-offset-current" %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/otp_unlock.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.otp_unlock_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 |
<%= rodauth.otp_unlock_consecutive_successes_label %>:
4 |
<%= rodauth.otp_unlock_num_successes %>
5 | 6 |
<%= rodauth.otp_unlock_required_consecutive_successes_label %>:
7 |
<%= rodauth.otp_unlock_auths_required %>
8 | 9 |
<%= rodauth.otp_unlock_next_auth_deadline_label %>:
10 |
<%= rodauth.otp_unlock_deadline.strftime(rodauth.strftime_format) %>
11 |
12 | 13 |
14 | <%= form.label "otp-auth-code", rodauth.otp_auth_label, class: "block text-sm font-semibold" %> 15 | <%= form.text_field rodauth.otp_auth_param, value: "", id: "otp-auth-code", autocomplete: "off", inputmode: "numeric", required: true, class: "mt-2 text-sm w-1/2 px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.otp_auth_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "otp-auth-code_error_message" } if rodauth.field_error(rodauth.otp_auth_param)) %> 16 | <%= content_tag(:span, rodauth.field_error(rodauth.otp_auth_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "otp-auth-code_error_message") if rodauth.field_error(rodauth.otp_auth_param) %> 17 |
18 | 19 | <%= form.submit rodauth.otp_unlock_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 20 | <% end %> 21 | 22 | <%== rodauth.otp_unlock_form_footer %> 23 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/otp_unlock_not_available.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
<%= rodauth.otp_unlock_consecutive_successes_label %>:
3 |
<%= rodauth.otp_unlock_num_successes %>
4 | 5 |
<%= rodauth.otp_unlock_required_consecutive_successes_label %>:
6 |
<%= rodauth.otp_unlock_auths_required %>
7 | 8 |
<%= rodauth.otp_unlock_next_auth_attempt_label %>:
9 |
<%= rodauth.otp_unlock_deadline.strftime(rodauth.strftime_format) %>
10 |
11 | 12 |

<%= rodauth.otp_unlock_next_auth_attempt_refresh_label %>

13 | 14 | <%== rodauth.otp_unlock_refresh_tag %> 15 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/recovery_auth.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.recovery_auth_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 | <%= form.label "recovery-code", rodauth.recovery_codes_label, class: "block text-sm font-semibold" %> 4 | <%= form.text_field rodauth.recovery_codes_param, value: "", id: "recovery-code", autocomplete: "off", required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.recovery_codes_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "recovery-code_error_message" } if rodauth.field_error(rodauth.recovery_codes_param)) %> 5 | <%= content_tag(:span, rodauth.field_error(rodauth.recovery_codes_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "recovery-code_error_message") if rodauth.field_error(rodauth.recovery_codes_param) %> 6 |
7 | 8 | <%= form.submit rodauth.recovery_auth_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/recovery_codes.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.recovery_codes_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 | <%= form.submit rodauth.recovery_codes_button || rodauth.view_recovery_codes_button, name: (rodauth.add_recovery_codes_param if rodauth.recovery_codes_button), class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/remember.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.remember_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 |
4 | <%= form.radio_button rodauth.remember_param, rodauth.remember_remember_param_value, id: "remember-remember", class: "dark:bg-gray-900 dark:border-gray-600 dark:checked:bg-current dark:checked:border-current dark:checked:text-emerald-400 dark:focus:ring-emerald-400 dark:focus:ring-offset-gray-900" %> 5 | <%= form.label "remember-remember", rodauth.remember_remember_label, class: "text-sm" %> 6 |
7 |
8 | <%= form.radio_button rodauth.remember_param, rodauth.remember_forget_param_value, id: "remember-forget", class: "dark:bg-gray-900 dark:border-gray-600 dark:checked:bg-current dark:checked:border-current dark:checked:text-emerald-400 dark:focus:ring-emerald-400 dark:focus:ring-offset-gray-900" %> 9 | <%= form.label "remember-forget", rodauth.remember_forget_label, class: "text-sm" %> 10 |
11 |
12 | <%= form.radio_button rodauth.remember_param, rodauth.remember_disable_param_value, id: "remember-disable", class: "dark:bg-gray-900 dark:border-gray-600 dark:checked:bg-current dark:checked:border-current dark:checked:text-emerald-400 dark:focus:ring-emerald-400 dark:focus:ring-offset-gray-900" %> 13 | <%= form.label "remember-disable", rodauth.remember_disable_label, class: "text-sm" %> 14 |
15 |
16 | 17 | <%= form.submit rodauth.remember_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 18 | <% end %> 19 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/reset_password.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.reset_password_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 4 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 5 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 6 |
7 | 8 | <% if rodauth.require_password_confirmation? %> 9 |
10 | <%= form.label "password-confirm", rodauth.password_confirm_label, class: "block text-sm font-semibold" %> 11 | <%= form.password_field rodauth.password_confirm_param, value: "", id: "password-confirm", autocomplete: "new-password", required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_confirm_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password-confirm_error_message" } if rodauth.field_error(rodauth.password_confirm_param)) %> 12 | <%= content_tag(:span, rodauth.field_error(rodauth.password_confirm_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password-confirm_error_message") if rodauth.field_error(rodauth.password_confirm_param) %> 13 |
14 | <% end %> 15 | 16 | <%= form.submit rodauth.reset_password_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/reset_password_request.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.reset_password_request_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 | <%== rodauth.reset_password_explanatory_text %> 4 |
5 | 6 | <% if params[rodauth.login_param] && !rodauth.field_error(rodauth.login_param) %> 7 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 8 | <% else %> 9 |
10 | <%= form.label "login", rodauth.login_label, class: "block text-sm font-semibold" %> 11 | <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", autocomplete: "email", required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.login_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "login_error_message" } if rodauth.field_error(rodauth.login_param)) %> 12 | <%= content_tag(:span, rodauth.field_error(rodauth.login_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "login_error_message") if rodauth.field_error(rodauth.login_param) %> 13 |
14 | <% end %> 15 | 16 | <%= form.submit rodauth.reset_password_request_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/sms_auth.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_auth_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 | <%= form.label "sms-code", rodauth.sms_code_label, class: "block text-sm font-semibold" %> 4 | <%= form.text_field rodauth.sms_code_param, value: "", id: "sms-code", autocomplete: "one-time-code", inputmode: "numeric", required: true, class: "mt-2 text-sm w-1/2 px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.sms_code_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "sms-code_error_message" } if rodauth.field_error(rodauth.sms_code_param)) %> 5 | <%= content_tag(:span, rodauth.field_error(rodauth.sms_code_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "sms-code_error_message") if rodauth.field_error(rodauth.sms_code_param) %> 6 |
7 | 8 | <%= form.submit rodauth.sms_auth_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/sms_confirm.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_confirm_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 | <%= form.label "sms-code", rodauth.sms_code_label, class: "block text-sm font-semibold" %> 4 | <%= form.text_field rodauth.sms_code_param, value: "", id: "sms-code", autocomplete: "one-time-code", inputmode: "numeric", required: true, class: "mt-2 text-sm w-1/2 px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.sms_code_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "sms-code_error_message" } if rodauth.field_error(rodauth.sms_code_param)) %> 5 | <%= content_tag(:span, rodauth.field_error(rodauth.sms_code_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "sms-code_error_message") if rodauth.field_error(rodauth.sms_code_param) %> 6 |
7 | 8 | <%= form.submit rodauth.sms_confirm_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/sms_disable.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_disable_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 | <%= form.submit rodauth.sms_disable_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/sms_request.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_request_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <%= form.submit rodauth.sms_request_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/sms_setup.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.sms_setup_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <%= form.label "sms-phone", rodauth.sms_phone_label, class: "block text-sm font-semibold" %> 12 | <%= form.telephone_field rodauth.sms_phone_param, value: "", id: "sms-phone", autocomplete: "tel", required: true, class: "mt-2 text-sm w-1/2 px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.sms_phone_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "sms-phone_error_message" } if rodauth.field_error(rodauth.sms_phone_param)) %> 13 | <%= content_tag(:span, rodauth.field_error(rodauth.sms_phone_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "sms-phone_error_message") if rodauth.field_error(rodauth.sms_phone_param) %> 14 |
15 | 16 | <%= form.submit rodauth.sms_setup_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/two_factor_auth.html.erb: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/two_factor_disable.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.two_factor_disable_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 | <%= form.submit rodauth.two_factor_disable_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-300 dark:bg-violet-400 dark:hover:bg-violet-500 dark:text-gray-900 dark:focus:ring-violet-400 dark:focus:ring-offset-current" %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/two_factor_manage.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if rodauth.two_factor_setup_links.any? %> 3 |
4 | <%== rodauth.two_factor_setup_heading %> 5 |
6 | 11 | <% end %> 12 | 13 | <% if rodauth.two_factor_remove_links.any? %> 14 |
15 | <%== rodauth.two_factor_remove_heading %> 16 |
17 | 25 | <% end %> 26 |
27 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/unlock_account.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.unlock_account_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 | <%== rodauth.unlock_account_explanatory_text %> 4 |
5 | 6 | <% if rodauth.unlock_account_requires_password? %> 7 |
8 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 9 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 10 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 11 |
12 | <% end %> 13 | 14 | <%= form.submit rodauth.unlock_account_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 15 | <% end %> 16 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/unlock_account_request.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.unlock_account_request_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 | <%== rodauth.unlock_account_request_explanatory_text %> 4 |
5 | 6 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 7 | 8 | <%= form.submit rodauth.unlock_account_request_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/verify_account.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.verify_account_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.verify_account_set_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | 9 | <% if rodauth.require_password_confirmation? %> 10 |
11 | <%= form.label "password-confirm", rodauth.password_confirm_label, class: "block text-sm font-semibold" %> 12 | <%= form.password_field rodauth.password_confirm_param, value: "", id: "password-confirm", autocomplete: "new-password", required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_confirm_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password-confirm_error_message" } if rodauth.field_error(rodauth.password_confirm_param)) %> 13 | <%= content_tag(:span, rodauth.field_error(rodauth.password_confirm_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password-confirm_error_message") if rodauth.field_error(rodauth.password_confirm_param) %> 14 |
15 | <% end %> 16 | <% end %> 17 | 18 | <%= form.submit rodauth.verify_account_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 19 | <% end %> 20 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/verify_account_resend.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.verify_account_resend_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 |
3 | <%== rodauth.verify_account_resend_explanatory_text %> 4 |
5 | 6 | <% if params[rodauth.login_param] %> 7 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 8 | <% else %> 9 |
10 | <%= form.label "login", rodauth.login_label, class: "block text-sm font-semibold" %> 11 | <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", autocomplete: "email", required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.login_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "login_error_message" } if rodauth.field_error(rodauth.login_param)) %> 12 | <%= content_tag(:span, rodauth.field_error(rodauth.login_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "login_error_message") if rodauth.field_error(rodauth.login_param) %> 13 |
14 | <% end %> 15 | 16 | <%= form.submit rodauth.verify_account_resend_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/verify_login_change.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.verify_login_change_path, method: :post, data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <%= form.submit rodauth.verify_login_change_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_auth.html.erb: -------------------------------------------------------------------------------- 1 | <% cred = rodauth.webauthn_credential_options_for_get %> 2 | 3 | <%= form_with url: rodauth.webauthn_auth_form_path, method: :post, id: "webauthn-auth-form", data: { credential_options: cred.as_json.to_json, turbo: false }, class: "w-full max-w-sm" do |form| %> 4 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 5 | <%= form.hidden_field rodauth.webauthn_auth_challenge_param, value: cred.challenge %> 6 | <%= form.hidden_field rodauth.webauthn_auth_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %> 7 | <%= form.text_field rodauth.webauthn_auth_param, value: "", id: "webauthn-auth", class: "hidden", aria: { hidden: "true" } %> 8 |
9 | <%= form.submit rodauth.webauthn_auth_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 10 |
11 | <% end %> 12 | 13 | <%= javascript_include_tag rodauth.webauthn_auth_js_path, extname: false %> 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_autofill.html.erb: -------------------------------------------------------------------------------- 1 | <% cred = rodauth.webauthn_credential_options_for_get %> 2 | 3 | <%= form_with url: rodauth.webauthn_login_path, method: :post, id: "webauthn-login-form", data: { credential_options: cred.as_json.to_json, turbo: false } do |form| %> 4 | <%= form.hidden_field rodauth.webauthn_auth_challenge_param, value: cred.challenge %> 5 | <%= form.hidden_field rodauth.webauthn_auth_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %> 6 | <%= form.text_field rodauth.webauthn_auth_param, value: "", id: "webauthn-auth", class: "hidden", aria: { hidden: "true" } %> 7 | <%= form.submit rodauth.webauthn_auth_button, class: "hidden" %> 8 | <% end %> 9 | 10 | <%= javascript_include_tag rodauth.webauthn_autofill_js_path, extname: false %> 11 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_remove.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.webauthn_remove_path, method: :post, id: "webauthn-remove-form", data: { turbo: false }, class: "w-full max-w-sm" do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <% rodauth.account_webauthn_usage.each do |id, last_use| %> 12 |
13 | <% last_use = last_use.strftime(rodauth.strftime_format) if last_use.is_a?(Time) %> 14 | <%= form.radio_button rodauth.webauthn_remove_param, id, id: "webauthn-remove-#{id}", class: "dark:bg-gray-900 dark:border-gray-600 dark:checked:bg-current dark:checked:border-current dark:checked:text-emerald-400 dark:focus:ring-emerald-400 dark:focus:ring-offset-gray-900" %> 15 | <%= form.label "webauthn-remove-#{id}", "Last use: #{last_use}", class: "text-sm" %> 16 |
17 | <% end %> 18 | <%= content_tag(:span, rodauth.field_error(rodauth.webauthn_remove_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "webauthn_remove_error_message") if rodauth.field_error(rodauth.webauthn_remove_param) %> 19 |
20 | 21 | <%= form.submit rodauth.webauthn_remove_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 22 | <% end %> 23 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_setup.html.erb: -------------------------------------------------------------------------------- 1 | <% cred = rodauth.new_webauthn_credential %> 2 | 3 | <%= form_with url: request.path, method: :post, id: "webauthn-setup-form", data: { credential_options: cred.as_json.to_json, turbo: false }, class: "w-full max-w-sm" do |form| %> 4 | <%= form.hidden_field rodauth.webauthn_setup_challenge_param, value: cred.challenge %> 5 | <%= form.hidden_field rodauth.webauthn_setup_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %> 6 | <%= form.text_field rodauth.webauthn_setup_param, value: "", id: "webauthn-setup", class: "hidden", aria: { hidden: "true" } %> 7 | 8 | <% if rodauth.two_factor_modifications_require_password? %> 9 |
10 | <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %> 11 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 12 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 13 |
14 | <% end %> 15 | 16 |
17 | <%= form.submit rodauth.webauthn_setup_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %> 18 |
19 | <% end %> 20 | 21 | <%= javascript_include_tag rodauth.webauthn_setup_js_path, extname: false %> 22 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/two_factor_auth.html.erb: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/two_factor_disable.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.two_factor_disable_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <%= form.submit rodauth.two_factor_disable_button, class: "btn btn-primary" %> 12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/two_factor_manage.html.erb: -------------------------------------------------------------------------------- 1 | <% if rodauth.two_factor_setup_links.any? %> 2 | <%== rodauth.two_factor_setup_heading %> 3 | 4 | 9 | <% end %> 10 | 11 | <% if rodauth.two_factor_remove_links.any? %> 12 | <%== rodauth.two_factor_remove_heading %> 13 | 14 | 22 | <% end %> 23 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/unlock_account.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.unlock_account_path, method: :post, data: { turbo: false } do |form| %> 2 | <%== rodauth.unlock_account_explanatory_text %> 3 | 4 | <% if rodauth.unlock_account_requires_password? %> 5 |
6 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 7 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 8 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 9 |
10 | <% end %> 11 | 12 |
13 | <%= form.submit rodauth.unlock_account_button, class: "btn btn-primary" %> 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/unlock_account_request.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.unlock_account_request_path, method: :post, data: { turbo: false } do |form| %> 2 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 3 | 4 | <%== rodauth.unlock_account_request_explanatory_text %> 5 | 6 |
7 | <%= form.submit rodauth.unlock_account_request_button, class: "btn btn-primary" %> 8 |
9 | <% end %> 10 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/verify_account.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.verify_account_path, method: :post, data: { turbo: false } do |form| %> 2 | <% if rodauth.verify_account_set_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | 9 | <% if rodauth.require_password_confirmation? %> 10 |
11 | <%= form.label "password-confirm", rodauth.password_confirm_label, class: "form-label" %> 12 | <%= form.password_field rodauth.password_confirm_param, value: "", id: "password-confirm", autocomplete: "new-password", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_confirm_param)}", aria: ({ invalid: true, describedby: "password-confirm_error_message" } if rodauth.field_error(rodauth.password_confirm_param)) %> 13 | <%= content_tag(:span, rodauth.field_error(rodauth.password_confirm_param), class: "invalid-feedback", id: "password-confirm_error_message") if rodauth.field_error(rodauth.password_confirm_param) %> 14 |
15 | <% end %> 16 | <% end %> 17 | 18 |
19 | <%= form.submit rodauth.verify_account_button, class: "btn btn-primary" %> 20 |
21 | <% end %> 22 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/verify_account_resend.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.verify_account_resend_path, method: :post, data: { turbo: false } do |form| %> 2 | <%== rodauth.verify_account_resend_explanatory_text %> 3 | 4 | <% if params[rodauth.login_param] %> 5 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 6 | <% else %> 7 |
8 | <%= form.label "login", rodauth.login_label, class: "form-label" %> 9 | <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", autocomplete: "email", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.login_param)}", aria: ({ invalid: true, describedby: "login_error_message" } if rodauth.field_error(rodauth.login_param)) %> 10 | <%= content_tag(:span, rodauth.field_error(rodauth.login_param), class: "invalid-feedback", id: "login_error_message") if rodauth.field_error(rodauth.login_param) %> 11 |
12 | <% end %> 13 | 14 |
15 | <%= form.submit rodauth.verify_account_resend_button, class: "btn btn-primary" %> 16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/verify_login_change.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.verify_login_change_path, method: :post, data: { turbo: false } do |form| %> 2 |
3 | <%= form.submit rodauth.verify_login_change_button, class: "btn btn-primary" %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/webauthn_auth.html.erb: -------------------------------------------------------------------------------- 1 | <% cred = rodauth.webauthn_credential_options_for_get %> 2 | 3 | <%= form_with url: rodauth.webauthn_auth_form_path, method: :post, id: "webauthn-auth-form", data: { credential_options: cred.as_json.to_json, turbo: false } do |form| %> 4 | <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %> 5 | <%= form.hidden_field rodauth.webauthn_auth_challenge_param, value: cred.challenge %> 6 | <%= form.hidden_field rodauth.webauthn_auth_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %> 7 | <%= form.text_field rodauth.webauthn_auth_param, value: "", id: "webauthn-auth", class: "d-none", aria: { hidden: "true" } %> 8 |
9 |
10 | <%= form.submit rodauth.webauthn_auth_button, class: "btn btn-primary" %> 11 |
12 |
13 | <% end %> 14 | 15 | <%= javascript_include_tag rodauth.webauthn_auth_js_path, extname: false %> 16 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/webauthn_autofill.html.erb: -------------------------------------------------------------------------------- 1 | <% cred = rodauth.webauthn_credential_options_for_get %> 2 | 3 | <%= form_with url: rodauth.webauthn_login_path, method: :post, id: "webauthn-login-form", data: { credential_options: cred.as_json.to_json, turbo: false } do |form| %> 4 | <%= form.hidden_field rodauth.webauthn_auth_challenge_param, value: cred.challenge %> 5 | <%= form.hidden_field rodauth.webauthn_auth_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %> 6 | <%= form.text_field rodauth.webauthn_auth_param, value: "", id: "webauthn-auth", class: "d-none", aria: { hidden: "true" } %> 7 | <%= form.submit rodauth.webauthn_auth_button, class: "d-none" %> 8 | <% end %> 9 | 10 | <%= javascript_include_tag rodauth.webauthn_autofill_js_path, extname: false %> 11 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: rodauth.webauthn_remove_path, method: :post, id: "webauthn-remove-form", data: { turbo: false } do |form| %> 2 | <% if rodauth.two_factor_modifications_require_password? %> 3 |
4 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 5 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 6 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 7 |
8 | <% end %> 9 | 10 |
11 | <% (usage = rodauth.account_webauthn_usage).each do |id, last_use| %> 12 |
13 | <% last_use = last_use.strftime(rodauth.strftime_format) if last_use.is_a?(Time) %> 14 | <%= form.radio_button rodauth.webauthn_remove_param, id, id: "webauthn-remove-#{id}", class: "form-check-input #{"is-invalid" if rodauth.field_error(rodauth.webauthn_remove_param)}", aria: ({ invalid: true, describedby: "webauthn_remove_error_message" } if rodauth.field_error(rodauth.webauthn_remove_param)) %> 15 | <%= form.label "webauthn-remove-#{id}", "Last use: #{last_use}", class: "form-check-label" %> 16 | <%= content_tag(:span, rodauth.field_error(rodauth.webauthn_remove_param), class: "invalid-feedback", id: "webauthn_remove_error_message") if rodauth.field_error(rodauth.webauthn_remove_param) && id == usage.keys.last %> 17 |
18 | <% end %> 19 |
20 | 21 |
22 | <%= form.submit rodauth.webauthn_remove_button, class: "btn btn-primary" %> 23 |
24 | <% end %> 25 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth/webauthn_setup.html.erb: -------------------------------------------------------------------------------- 1 | <% cred = rodauth.new_webauthn_credential %> 2 | 3 | <%= form_with url: request.path, method: :post, id: "webauthn-setup-form", data: { credential_options: cred.as_json.to_json, turbo: false } do |form| %> 4 | <%= form.hidden_field rodauth.webauthn_setup_challenge_param, value: cred.challenge %> 5 | <%= form.hidden_field rodauth.webauthn_setup_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %> 6 | <%= form.text_field rodauth.webauthn_setup_param, value: "", id: "webauthn-setup", class: "d-none", aria: { hidden: "true" } %> 7 | 8 | <% if rodauth.two_factor_modifications_require_password? %> 9 |
10 | <%= form.label "password", rodauth.password_label, class: "form-label" %> 11 | <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> 12 | <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> 13 |
14 | <% end %> 15 | 16 |
17 |
18 | <%= form.submit rodauth.webauthn_setup_button, class: "btn btn-primary" %> 19 |
20 |
21 | <% end %> 22 | 23 | <%= javascript_include_tag rodauth.webauthn_setup_js_path, extname: false %> 24 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/email_auth.text.erb: -------------------------------------------------------------------------------- 1 | Someone has requested a login link for the account with this email 2 | address. If you did not request a login link, please ignore this 3 | message. If you requested a login link, please go to 4 | <%= @rodauth.email_auth_email_link %> 5 | to login to this account. 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/otp_disabled.text.erb: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has disabled TOTP authentication for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/otp_locked_out.text.erb: -------------------------------------------------------------------------------- 1 | TOTP authentication has been locked out on your account due to too many 2 | consecutive authentication failures. You can attempt to unlock TOTP 3 | authentication for your account by consecutively authenticating via 4 | TOTP multiple times. 5 | 6 | If you did not initiate the TOTP authentication failures that 7 | caused TOTP authentication to be locked out, that means someone already 8 | has partial access to your account, but is unable to use TOTP 9 | authentication to fully authenticate themselves. 10 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/otp_setup.text.erb: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has setup TOTP authentication for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/otp_unlock_failed.text.erb: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) attempted to unlock TOTP authentication for the 2 | account associated to this email address, but failed as the 3 | authentication code submitted was not correct. 4 | 5 | If you did not initiate the TOTP authentication failure that generated 6 | this email, that means someone already has partial access to your 7 | account, but is unable to use TOTP authentication to fully authenticate 8 | themselves. 9 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/otp_unlocked.text.erb: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has unlocked TOTP authentication for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/password_changed.text.erb: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has changed the password for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/reset_password.text.erb: -------------------------------------------------------------------------------- 1 | Someone has requested a password reset for the account with this email 2 | address. If you did not request a password reset, please ignore this 3 | message. If you requested a password reset, please go to 4 | <%= @rodauth.reset_password_email_link %> 5 | to reset the password for the account. 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/reset_password_notify.text.erb: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has reset the password for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb: -------------------------------------------------------------------------------- 1 | Someone has requested that the account with this email be unlocked. 2 | If you did not request the unlocking of this account, please ignore this 3 | message. If you requested the unlocking of this account, please go to 4 | <%= @rodauth.unlock_account_email_link %> 5 | to unlock this account. 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/verify_account.text.erb: -------------------------------------------------------------------------------- 1 | Someone has created an account with this email address. If you did not create 2 | this account, please ignore this message. If you created this account, please go to 3 | <%= @rodauth.verify_account_email_link %> 4 | to verify the account. 5 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/verify_login_change.text.erb: -------------------------------------------------------------------------------- 1 | Someone with an account has requested their login be changed to this email address: 2 | 3 | Old email: <%= @account.email %> 4 | 5 | New email: <%= @new_email %> 6 | 7 | If you did not request this login change, please ignore this message. If you 8 | requested this login change, please go to 9 | <%= @rodauth.verify_login_change_email_link %> 10 | to verify the login change. 11 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/webauthn_authenticator_added.text.erb: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has added a WebAuthn authenticator to the 2 | account associated to this email address. There are now <%= @account.webauthn_keys.count %> WebAuthn 3 | authenticator(s) with access to the account. 4 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/app/views/rodauth_mailer/webauthn_authenticator_removed.text.erb: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has removed a WebAuthn authenticator from the 2 | account associated to this email address. There are now <%= @account.webauthn_keys.count %> WebAuthn 3 | authenticator(s) with access to the account. 4 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/config/initializers/rodauth.rb.tt: -------------------------------------------------------------------------------- 1 | Rodauth::Rails.configure do |config| 2 | config.app = "RodauthApp" 3 | # config.middleware = false # disable auto-insertion of Rodauth middleware 4 | # config.tilt = false # skip loading Tilt gem for rendering built-in templates 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/db/migrate/create_rodauth.rb.tt: -------------------------------------------------------------------------------- 1 | <% if defined?(::ActiveRecord::Railtie) -%> 2 | class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] 3 | def change 4 | <%= migration_content -%> 5 | end 6 | end 7 | <% else -%> 8 | Sequel.migration do 9 | change do 10 | <%= migration_content -%> 11 | end 12 | end 13 | <% end -%> 14 | -------------------------------------------------------------------------------- /lib/generators/rodauth/templates/test/fixtures/accounts.yml.tt: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | one: 3 | email: freddie@queen.com 4 | password_hash: <%%= RodauthApp.rodauth.allocate.password_hash("password") %> 5 | status: verified 6 | 7 | two: 8 | email: brian@queen.com 9 | password_hash: <%%= RodauthApp.rodauth.allocate.password_hash("password") %> 10 | status: verified 11 | -------------------------------------------------------------------------------- /lib/rodauth-rails.rb: -------------------------------------------------------------------------------- 1 | require "rodauth/rails" 2 | -------------------------------------------------------------------------------- /lib/rodauth/rails.rb: -------------------------------------------------------------------------------- 1 | require "rodauth/rails/version" 2 | require "rodauth/rails/railtie" 3 | require "rodauth/model" 4 | 5 | module Rodauth 6 | module Rails 7 | class Error < StandardError 8 | end 9 | 10 | # This allows avoiding loading Rodauth at boot time. 11 | autoload :App, "rodauth/rails/app" 12 | autoload :Auth, "rodauth/rails/auth" 13 | autoload :Mailer, "rodauth/rails/mailer" 14 | 15 | @app = nil 16 | @middleware = true 17 | @tilt = true 18 | 19 | class << self 20 | def lib(**options, &block) 21 | c = Class.new(Rodauth::Rails::App) 22 | c.configure(json: false, **options) do 23 | enable :internal_request 24 | instance_exec(&block) 25 | end 26 | c.freeze 27 | c.rodauth 28 | end 29 | 30 | def rodauth(name = nil, account: nil, **options) 31 | auth_class = app.rodauth!(name) 32 | 33 | unless auth_class.features.include?(:internal_request) 34 | fail Rodauth::Rails::Error, "Rodauth::Rails.rodauth requires internal_request feature to be enabled" 35 | end 36 | 37 | if account 38 | options[:account_id] = account.id 39 | end 40 | 41 | instance = auth_class.internal_request_eval(options) do 42 | if defined?(ActiveRecord::Base) && account.is_a?(ActiveRecord::Base) 43 | @account = account.attributes_before_type_cast.symbolize_keys 44 | elsif defined?(Sequel::Model) && account.is_a?(Sequel::Model) 45 | @account = account.values 46 | end 47 | self 48 | end 49 | 50 | # clean up inspect output 51 | instance.remove_instance_variable(:@internal_request_block) 52 | instance.remove_instance_variable(:@internal_request_return_value) 53 | 54 | instance 55 | end 56 | 57 | def model(name = nil, **options) 58 | Rodauth::Model.new(app.rodauth!(name), **options) 59 | end 60 | 61 | # Routing constraint that requires authenticated account. 62 | def authenticate(name = nil, &condition) 63 | lambda do |request| 64 | rodauth = request.env.fetch ["rodauth", *name].join(".") 65 | rodauth.require_account 66 | condition.nil? || condition.call(rodauth) 67 | end 68 | end 69 | 70 | if ::Rails.gem_version >= Gem::Version.new("5.2") 71 | def secret_key_base 72 | ::Rails.application.secret_key_base 73 | end 74 | else 75 | def secret_key_base 76 | ::Rails.application.secrets.secret_key_base 77 | end 78 | end 79 | 80 | def configure 81 | yield self 82 | end 83 | 84 | attr_writer :app 85 | attr_writer :middleware 86 | attr_writer :tilt 87 | 88 | def app 89 | fail Rodauth::Rails::Error, "app was not configured" unless @app 90 | 91 | @app.constantize 92 | end 93 | 94 | def middleware? 95 | @middleware 96 | end 97 | 98 | def tilt? 99 | @tilt 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/rodauth/rails/auth.rb: -------------------------------------------------------------------------------- 1 | require "rodauth" 2 | require "rodauth/rails/feature" 3 | 4 | module Rodauth 5 | module Rails 6 | # Base auth class that applies some changes to the default configuration. 7 | class Auth < Rodauth::Auth 8 | configure do 9 | enable :rails 10 | 11 | # database functions are more complex to set up, so disable them by default 12 | use_database_authentication_functions? false 13 | 14 | # avoid having to set deadline values in column default values 15 | set_deadline_values? true 16 | 17 | # use HMACs for additional security 18 | hmac_secret { Rodauth::Rails.secret_key_base } 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/rodauth/rails/controller_methods.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | module ControllerMethods 4 | def self.included(controller) 5 | # ActionController::API doesn't have helper methods 6 | if controller.respond_to?(:helper_method) 7 | controller.helper_method :rodauth 8 | end 9 | end 10 | 11 | def rodauth(name = nil) 12 | request.env.fetch ["rodauth", *name].join(".") 13 | end 14 | 15 | private 16 | 17 | # Adds response status to instrumentation payload for logging, 18 | # when calling a halting rodauth method inside a controller. 19 | def append_info_to_payload(payload) 20 | super 21 | if request.env["rodauth.rails.status"] 22 | payload[:status] = request.env.delete("rodauth.rails.status") 23 | end 24 | end 25 | 26 | def rodauth_response 27 | res = catch(:halt) { return yield } 28 | 29 | self.status = res[0] 30 | self.headers.merge! res[1] 31 | self.response_body = res[2] 32 | 33 | res 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/rodauth/rails/feature.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | Feature.define(:rails) do 3 | # Assign feature and feature configuration to constants for introspection. 4 | Rodauth::Rails::Feature = self 5 | Rodauth::Rails::FeatureConfiguration = self.configuration 6 | 7 | require "rodauth/rails/feature/base" 8 | require "rodauth/rails/feature/callbacks" 9 | require "rodauth/rails/feature/csrf" 10 | require "rodauth/rails/feature/render" 11 | require "rodauth/rails/feature/email" if defined?(ActionMailer) 12 | require "rodauth/rails/feature/instrumentation" 13 | require "rodauth/rails/feature/internal_request" 14 | 15 | include Rodauth::Rails::Feature::Base 16 | include Rodauth::Rails::Feature::Callbacks 17 | include Rodauth::Rails::Feature::Csrf 18 | include Rodauth::Rails::Feature::Render 19 | include Rodauth::Rails::Feature::Email if defined?(ActionMailer) 20 | include Rodauth::Rails::Feature::Instrumentation 21 | include Rodauth::Rails::Feature::InternalRequest 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rodauth/rails/feature/base.rb: -------------------------------------------------------------------------------- 1 | require "active_support/concern" 2 | 3 | module Rodauth 4 | module Rails 5 | module Feature 6 | module Base 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | auth_methods :rails_controller 11 | auth_value_methods :rails_account_model 12 | auth_cached_method :rails_controller_instance 13 | end 14 | 15 | def rails_account 16 | @rails_account = nil if account.nil? || @rails_account&.id != account_id 17 | @rails_account ||= instantiate_rails_account if account! 18 | end 19 | 20 | # Reset Rails session to protect from session fixation attacks. 21 | def clear_session 22 | rails_controller_instance.reset_session 23 | end 24 | 25 | # Default the flash error key to Rails' default :alert. 26 | def flash_error_key 27 | :alert 28 | end 29 | 30 | # Evaluates the block in context of a Rodauth controller instance. 31 | def rails_controller_eval(&block) 32 | rails_controller_instance.instance_exec(&block) 33 | end 34 | 35 | def rails_controller 36 | if only_json? && ::Rails.configuration.api_only 37 | ActionController::API 38 | else 39 | ActionController::Base 40 | end 41 | end 42 | 43 | def rails_account_model 44 | table = accounts_table 45 | table = table.column if table.is_a?(Sequel::SQL::QualifiedIdentifier) # schema is specified 46 | table.to_s.classify.constantize 47 | rescue NameError 48 | raise Error, "cannot infer account model, please set `rails_account_model` in your rodauth configuration" 49 | end 50 | 51 | delegate :rails_routes, :rails_cookies, :rails_request, to: :scope 52 | 53 | def session 54 | super 55 | rescue Roda::RodaError 56 | fail Rodauth::Rails::Error, "There is no session middleware configured, see instructions on how to add it: https://guides.rubyonrails.org/api_app.html#using-session-middlewares" 57 | end 58 | 59 | private 60 | 61 | def instantiate_rails_account 62 | if defined?(ActiveRecord::Base) && rails_account_model < ActiveRecord::Base 63 | if account_id 64 | rails_account_model.instantiate(account.stringify_keys) 65 | else 66 | rails_account_model.new(account) 67 | end 68 | elsif defined?(Sequel::Model) && rails_account_model < Sequel::Model 69 | rails_account_model.load(account) 70 | else 71 | fail Error, "unsupported model type: #{rails_account_model}" 72 | end 73 | end 74 | 75 | # Instance of the configured controller with current request's env hash. 76 | def _rails_controller_instance 77 | controller = rails_controller.new 78 | controller.set_request! rails_request 79 | controller.set_response! rails_controller.make_response!(controller.request) 80 | controller 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/rodauth/rails/feature/callbacks.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | module Feature 4 | module Callbacks 5 | extend ActiveSupport::Concern 6 | 7 | private 8 | 9 | def _around_rodauth 10 | rails_controller_instance.instance_variable_set(:@_action_name, current_route.to_s) 11 | 12 | rails_controller_around { super } 13 | end 14 | 15 | # Runs controller callbacks and rescue handlers around Rodauth actions. 16 | def rails_controller_around 17 | result = nil 18 | 19 | rails_controller_rescue do 20 | rails_controller_callbacks do 21 | result = catch(:halt) { yield } 22 | end 23 | end 24 | 25 | result = handle_rails_controller_response(result) 26 | 27 | throw :halt, result if result 28 | end 29 | 30 | # Runs any #(before|around|after)_action controller callbacks. 31 | def rails_controller_callbacks(&block) 32 | rails_controller_instance.run_callbacks(:process_action, &block) 33 | end 34 | 35 | # Runs any registered #rescue_from controller handlers. 36 | def rails_controller_rescue 37 | yield 38 | rescue Exception => exception 39 | rails_controller_instance.rescue_with_handler(exception) || raise 40 | 41 | unless rails_controller_instance.performed? 42 | raise Rodauth::Rails::Error, "rescue_from handler didn't write any response" 43 | end 44 | end 45 | 46 | # Handles controller rendering a response or setting response headers. 47 | def handle_rails_controller_response(result) 48 | if rails_controller_instance.performed? 49 | rails_controller_response 50 | elsif result 51 | result[1].merge!(rails_controller_instance.response.headers) 52 | result 53 | end 54 | end 55 | 56 | # Returns Roda response from controller response if set. 57 | def rails_controller_response 58 | controller_response = rails_controller_instance.response 59 | 60 | response.status = controller_response.status 61 | response.headers.merge! controller_response.headers 62 | response.write controller_response.body 63 | 64 | response.finish 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/rodauth/rails/feature/csrf.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | module Feature 4 | module Csrf 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | auth_methods( 9 | :rails_csrf_tag, 10 | :rails_csrf_param, 11 | :rails_csrf_token, 12 | :rails_check_csrf!, 13 | ) 14 | end 15 | 16 | # Render Rails CSRF tags in Rodauth templates. 17 | def csrf_tag(*) 18 | rails_csrf_tag if rails_controller_csrf? 19 | end 20 | 21 | # Verify Rails' authenticity token. 22 | def check_csrf 23 | rails_check_csrf! if rails_controller_csrf? 24 | end 25 | 26 | # Have Rodauth call #check_csrf automatically. 27 | def check_csrf? 28 | rails_check_csrf? if rails_controller_csrf? 29 | end 30 | 31 | private 32 | 33 | def rails_controller_callbacks 34 | return super unless rails_controller_csrf? 35 | 36 | # don't verify CSRF token as part of callbacks, Rodauth will do that 37 | rails_controller_instance.allow_forgery_protection = false 38 | super do 39 | # turn the setting back to default so that form tags generate CSRF tags 40 | rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection 41 | yield 42 | end 43 | end 44 | 45 | # Checks whether ActionController::RequestForgeryProtection is included 46 | # and that protect_from_forgery was called. 47 | def rails_check_csrf? 48 | !!rails_controller_instance.forgery_protection_strategy 49 | end 50 | 51 | # Calls the controller to verify the authenticity token. 52 | def rails_check_csrf! 53 | rails_controller_instance.send(:verify_authenticity_token) 54 | end 55 | 56 | # Hidden tag with Rails CSRF token inserted into Rodauth templates. 57 | def rails_csrf_tag 58 | %() 59 | end 60 | 61 | # The request parameter under which to send the Rails CSRF token. 62 | def rails_csrf_param 63 | rails_controller.request_forgery_protection_token 64 | end 65 | 66 | # The Rails CSRF token value inserted into Rodauth templates. 67 | def rails_csrf_token 68 | rails_controller_instance.send(:form_authenticity_token) 69 | end 70 | 71 | # Checks whether ActionController::RequestForgeryProtection is included. 72 | def rails_controller_csrf? 73 | rails_controller.respond_to?(:protect_from_forgery) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/rodauth/rails/feature/email.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | module Feature 4 | module Email 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | depends :email_base 9 | end 10 | 11 | private 12 | 13 | # Create emails with ActionMailer which uses configured delivery method. 14 | def create_email_to(to, subject, body) 15 | Rodauth::Rails::Mailer.create_email( 16 | to: to, 17 | from: email_from, 18 | subject: "#{email_subject_prefix}#{subject}", 19 | body: body 20 | ) 21 | end 22 | 23 | # Delivers the given email. 24 | def send_email(email) 25 | email.deliver_now 26 | end 27 | 28 | # for backwards compatibility 29 | Mailer = Rodauth::Rails::Mailer 30 | deprecate_constant :Mailer 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/rodauth/rails/feature/internal_request.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | module Feature 4 | module InternalRequest 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | auth_private_methods :rails_url_options 9 | end 10 | 11 | def domain 12 | return super unless missing_host? && rails_url_options! 13 | 14 | rails_url_options.fetch(:host) 15 | end 16 | 17 | def base_url 18 | return super unless missing_host? && domain && rails_url_options! 19 | 20 | scheme = rails_url_options[:protocol] || "http" 21 | port = rails_url_options[:port] 22 | 23 | url = "#{scheme}://#{domain}" 24 | url << ":#{port}" if port 25 | url 26 | end 27 | 28 | def rails_url_options 29 | return _rails_url_options if frozen? # handle path_class_methods feature 30 | @rails_url_options ||= _rails_url_options 31 | end 32 | 33 | private 34 | 35 | def rails_controller_around 36 | return yield if internal_request? 37 | super 38 | end 39 | 40 | def rails_instrument_request 41 | return yield if internal_request? 42 | super 43 | end 44 | 45 | def rails_instrument_redirection 46 | return yield if internal_request? 47 | super 48 | end 49 | 50 | # Checks whether we're in an internal request and host was not set, 51 | # or the request doesn't exist such as with path_class_methods feature. 52 | def missing_host? 53 | internal_request? && (request.host.nil? || request.host == INVALID_DOMAIN) || scope.nil? 54 | end 55 | 56 | def rails_url_options! 57 | rails_url_options.presence or fail Error, "There is no information to set the URL host from. Please set config.action_mailer.default_url_options in your Rails application, configure #domain and #base_url in your Rodauth configuration, or set #rails_url_options on the Rodauth instance." 58 | end 59 | 60 | def _rails_url_options 61 | defined?(ActionMailer) ? ActionMailer::Base.default_url_options.dup : {} 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/rodauth/rails/feature/render.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | module Feature 4 | module Render 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | auth_methods :rails_render 9 | end 10 | 11 | # Renders templates with layout. First tries to render a user-defined 12 | # template, otherwise falls back to Rodauth's template. 13 | def view(page, title) 14 | set_title(title) 15 | rails_render(action: page.tr("-", "_"), layout: true) || 16 | rails_render(html: super.html_safe, layout: true, formats: :html) 17 | end 18 | 19 | # Renders templates without layout. First tries to render a user-defined 20 | # template or partial, otherwise falls back to Rodauth's template. 21 | def render(page) 22 | rails_render(partial: page.tr("-", "_"), layout: false) || 23 | rails_render(action: page.tr("-", "_"), layout: false) || 24 | super.html_safe 25 | end 26 | 27 | def button(*) 28 | super.html_safe 29 | end 30 | 31 | if defined?(::Turbo) 32 | def turbo_stream 33 | rails_controller_instance.send(:turbo_stream) 34 | end 35 | end 36 | 37 | private 38 | 39 | # Calls the Rails renderer, returning nil if a template is missing. 40 | def rails_render(*args) 41 | return if rails_controller <= ActionController::API 42 | 43 | rails_controller_instance.render_to_string(*args) 44 | rescue ActionView::MissingTemplate 45 | nil 46 | end 47 | 48 | # Only look up template formats that the current request is accepting. 49 | def before_rodauth 50 | super 51 | rails_controller_instance.formats = rails_request.formats.map(&:ref).compact 52 | end 53 | 54 | # Not all Rodauth actions are Turbo-compatible (some form submissions 55 | # render 200 HTML responses), so we disable Turbo on all Rodauth forms. 56 | def _view(meth, *) 57 | html = super 58 | html = html.gsub(/]+)>/, '') if meth == :view 59 | html 60 | end 61 | 62 | def set_title(title) 63 | if title_instance_variable 64 | rails_controller_instance.instance_variable_set(title_instance_variable, title) 65 | end 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/rodauth/rails/mailer.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | class Mailer < ActionMailer::Base 4 | def create_email(options) 5 | mail(options) 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/rodauth/rails/middleware.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | # Middleware that's added to the Rails middleware stack. Normally the main 4 | # Roda app could be used directly, but this trick allows the app class to 5 | # be reloadable. 6 | class Middleware 7 | def initialize(app) 8 | @app = app 9 | end 10 | 11 | def call(env) 12 | return @app.call(env) if asset_request?(env) 13 | 14 | app = Rodauth::Rails.app.new(@app) 15 | 16 | # allow the Rails app to call Rodauth methods that throw :halt 17 | catch(:halt) do 18 | app.call(env) 19 | end 20 | end 21 | 22 | # Check whether it's a request to an asset managed by Sprockets or Propshaft. 23 | def asset_request?(env) 24 | return false unless ::Rails.configuration.respond_to?(:assets) 25 | 26 | env["PATH_INFO"] =~ %r(\A/{0,2}#{::Rails.configuration.assets.prefix}) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rodauth/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rodauth/rails/middleware" 2 | require "rodauth/rails/controller_methods" 3 | require "rodauth/rails/test" 4 | 5 | require "rails" 6 | 7 | module Rodauth 8 | module Rails 9 | class Railtie < ::Rails::Railtie 10 | initializer "rodauth.middleware", after: :load_config_initializers do |app| 11 | if Rodauth::Rails.middleware? 12 | app.middleware.use Rodauth::Rails::Middleware 13 | end 14 | end 15 | 16 | initializer "rodauth.controller" do 17 | ActiveSupport.on_load(:action_controller) do 18 | include Rodauth::Rails::ControllerMethods 19 | end 20 | end 21 | 22 | initializer "rodauth.test" do 23 | # Rodauth uses RACK_ENV to set the default bcrypt hash cost 24 | ENV["RACK_ENV"] = "test" if ::Rails.env.test? 25 | 26 | ActiveSupport.on_load(:action_controller_test_case) do 27 | include Rodauth::Rails::Test::Controller 28 | end 29 | end 30 | 31 | rake_tasks do 32 | load "rodauth/rails/tasks.rake" 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rodauth/rails/tasks.rake: -------------------------------------------------------------------------------- 1 | require "rodauth/rails/tasks/routes" 2 | 3 | namespace :rodauth do 4 | desc "Lists endpoints that will be routed by your Rodauth app" 5 | task routes: :environment do 6 | puts "Routes handled by #{Rodauth::Rails.app}:" 7 | 8 | Rodauth::Rails.app.opts[:rodauths].each_value do |auth_class| 9 | Rodauth::Rails::Tasks::Routes.new(auth_class).call 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/rodauth/rails/tasks/routes.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | module Tasks 4 | class Routes 5 | IGNORE = [:webauthn_setup_js, :webauthn_auth_js, :webauthn_autofill_js] 6 | JSON_POST = [:two_factor_manage, :two_factor_auth] 7 | 8 | attr_reader :auth_class 9 | 10 | def initialize(auth_class) 11 | @auth_class = auth_class 12 | end 13 | 14 | def call 15 | routes = auth_class.route_hash.map do |path, handle_method| 16 | route_name = handle_method.to_s.delete_prefix("handle_").to_sym 17 | next if IGNORE.include?(route_name) 18 | verbs = route_verbs(route_name) 19 | 20 | [ 21 | route_name.to_s, 22 | verbs.join("|"), 23 | "#{rodauth.prefix}#{path}", 24 | "rodauth#{configuration_name && "(:#{configuration_name})"}.#{route_name}_path", 25 | ] 26 | end 27 | 28 | routes.compact! 29 | padding = routes.transpose.map { |string| string.map(&:length).max } 30 | 31 | output_lines = routes.map do |columns| 32 | [columns[0].rjust(padding[0]), columns[1].ljust(padding[1]), columns[2].ljust(padding[2]), columns[3]].join(" ") 33 | end 34 | 35 | puts "\n #{output_lines.join("\n ")}" 36 | end 37 | 38 | private 39 | 40 | def route_verbs(route_name) 41 | file_path, start_line = rodauth.method(:"_handle_#{route_name}").source_location 42 | lines = File.foreach(file_path).to_a 43 | indentation = lines[start_line - 1][/^\s+/] 44 | verbs = [] 45 | 46 | lines[start_line..-1].each do |code| 47 | verbs << :GET if code.include?("r.get") && !rodauth.only_json? 48 | verbs << :POST if code.include?("r.post") 49 | break if code.start_with?("#{indentation}end") 50 | end 51 | 52 | verbs << :POST if rodauth.features.include?(:json) && JSON_POST.include?(route_name) 53 | verbs 54 | end 55 | 56 | def rodauth 57 | auth_class.new(scope) 58 | end 59 | 60 | def scope 61 | auth_class.roda_class.new({}) 62 | end 63 | 64 | def configuration_name 65 | auth_class.configuration_name 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/rodauth/rails/test.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | module Test 4 | autoload :Controller, "rodauth/rails/test/controller" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/rodauth/rails/test/controller.rb: -------------------------------------------------------------------------------- 1 | require "active_support/concern" 2 | 3 | module Rodauth 4 | module Rails 5 | module Test 6 | module Controller 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | setup :setup_rodauth 11 | end 12 | 13 | def process(*) 14 | catch_rodauth { super } 15 | end 16 | ruby2_keywords(:process) if respond_to?(:ruby2_keywords, true) 17 | 18 | private 19 | 20 | def setup_rodauth 21 | Rodauth::Rails.app.opts[:rodauths].each do |name, auth_class| 22 | scope = auth_class.roda_class.new(request.env) 23 | request.env[["rodauth", *name].join(".")] = auth_class.new(scope) 24 | end 25 | end 26 | 27 | def catch_rodauth(&block) 28 | result = catch(:halt, &block) 29 | 30 | if result.is_a?(Array) # rodauth response 31 | response.status = result[0] 32 | response.headers.merge! result[1] 33 | response.body = result[2] 34 | end 35 | 36 | response 37 | end 38 | 39 | def rodauth(name = nil) 40 | @controller.rodauth(name) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/rodauth/rails/version.rb: -------------------------------------------------------------------------------- 1 | module Rodauth 2 | module Rails 3 | VERSION = "2.1.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /rodauth-rails.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/rodauth/rails/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "rodauth-rails" 5 | spec.version = Rodauth::Rails::VERSION 6 | spec.authors = ["Janko Marohnić"] 7 | spec.email = ["janko.marohnic@gmail.com"] 8 | 9 | spec.summary = %q{Provides Rails integration for Rodauth authentication framework.} 10 | spec.description = %q{Provides Rails integration for Rodauth authentication framework.} 11 | spec.homepage = "https://github.com/janko/rodauth-rails" 12 | spec.license = "MIT" 13 | 14 | spec.required_ruby_version = ">= 2.6" 15 | 16 | spec.files = Dir["README.md", "LICENSE.txt", "lib/**/*", "*.gemspec"] 17 | spec.require_paths = ["lib"] 18 | 19 | spec.add_dependency "railties", ">= 5.1", "< 8.1" 20 | spec.add_dependency "rodauth", "~> 2.36" 21 | spec.add_dependency "roda", "~> 3.76" 22 | spec.add_dependency "rodauth-model", "~> 0.2" 23 | 24 | spec.add_development_dependency "tilt" 25 | spec.add_development_dependency "bcrypt", "~> 3.1" 26 | spec.add_development_dependency "jwt" 27 | spec.add_development_dependency "rotp" 28 | spec.add_development_dependency "rqrcode" 29 | spec.add_development_dependency "webauthn" unless RUBY_ENGINE == "jruby" 30 | end 31 | -------------------------------------------------------------------------------- /test/controllers/test_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TestControllerTest < ActionController::TestCase 4 | include TestSetupTeardown 5 | 6 | test "integration" do 7 | get :auth2 8 | assert_response 302 9 | assert_redirected_to "/login" 10 | assert_equal "Please login to continue", flash[:alert] 11 | 12 | account = Account.create!(email: "user@example.com", password: "secret", status: "verified") 13 | login(account) 14 | 15 | get :auth2 16 | assert_response 200 17 | 18 | # session state is retained on further requests 19 | get :auth2 20 | assert_response 200 21 | 22 | logout 23 | 24 | get :auth2 25 | assert_response 302 26 | assert_equal "Please login to continue", flash[:alert] 27 | end 28 | 29 | private 30 | 31 | def login(account) 32 | rodauth.account_from_login(account.email) 33 | rodauth.login_session("password") 34 | end 35 | 36 | def logout 37 | rodauth.logout 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/generators/migration_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/rodauth/migration_generator" 3 | 4 | class MigrationGeneratorTest < Rails::Generators::TestCase 5 | tests Rodauth::Rails::Generators::MigrationGenerator 6 | destination File.expand_path("#{__dir__}/../../tmp") 7 | setup :prepare_destination 8 | 9 | test "migration" do 10 | output = run_generator %w[otp sms_codes recovery_codes] 11 | 12 | migration_file = "db/migrate/create_rodauth_otp_sms_codes_recovery_codes.rb" 13 | migration_version = Regexp.escape("[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]") 14 | 15 | assert_migration migration_file, /class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration#{migration_version}/ 16 | assert_migration migration_file, /create_table :account_otp_keys, id: false do/ 17 | assert_migration migration_file, /create_table :account_sms_codes, id: false do/ 18 | assert_migration migration_file, /create_table :account_recovery_codes, primary_key: \[:id, :code\] do/ 19 | assert_migration migration_file, /t\.integer :id, primary_key: true/ 20 | 21 | refute_includes output, "Rodauth configuration" 22 | end 23 | 24 | test "migration name" do 25 | run_generator %w[email_auth --name create_account_email_auth_keys] 26 | 27 | assert_migration "db/migrate/create_account_email_auth_keys.rb", /class CreateAccountEmailAuthKeys/ 28 | end 29 | 30 | test "prefix" do 31 | output = run_generator %w[active_sessions base --prefix user] 32 | assert_migration "db/migrate/create_rodauth_user_active_sessions_base.rb", /create_table :user_active_session_keys/ 33 | assert_includes output, <<-EOS 34 | active_sessions_table :user_active_session_keys 35 | active_sessions_account_id_column :user_id 36 | accounts_table :users 37 | EOS 38 | 39 | run_generator %w[remember --prefix admins] 40 | assert_migration "db/migrate/create_rodauth_admins_remember.rb", /create_table :admin_remember_keys/ 41 | end 42 | 43 | test "migration uuid" do 44 | Rails.configuration.generators do |g| 45 | g.orm :active_record, primary_key_type: :uuid 46 | end 47 | 48 | run_generator %w[otp] 49 | 50 | migration_file = "db/migrate/create_rodauth_otp.rb" 51 | 52 | assert_migration migration_file, /t\.uuid :id, primary_key: true/ 53 | 54 | Rails.configuration.generators.options[:active_record].delete(:primary_key_type) 55 | end 56 | 57 | test "current timestamp column default" do 58 | run_generator %w[email_auth] 59 | 60 | assert_migration "db/migrate/create_rodauth_email_auth.rb", /default: -> { "CURRENT_TIMESTAMP" }/ 61 | end 62 | 63 | test "no features" do 64 | output = run_generator %w[] 65 | 66 | assert_equal "No features specified!\n", output 67 | assert_no_file "db/migrate/create_rodauth_.rb" 68 | end 69 | 70 | test "invalid features" do 71 | output = run_generator %w[sms_codes active_session totp] 72 | 73 | assert_equal "No available migration for feature(s): active_session, totp\n", output 74 | assert_no_file "db/migrate/create_rodauth_otp.rb" 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/integration/assets_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AssetsTest < IntegrationTest 4 | teardown do 5 | Rails.configuration.class.class_variable_get(:@@options).delete(:assets) 6 | end 7 | 8 | test "skips rodauth app for asset requests" do 9 | Rails.configuration.assets = Struct.new(:prefix).new("/assets") 10 | 11 | assert_raises ActionController::RoutingError do 12 | visit "/assets/foo" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/integration/callbacks_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CallbacksTest < IntegrationTest 4 | test "runs callbacks" do 5 | visit "/login" 6 | 7 | assert_match "login-form", page.html 8 | assert_equal 200, page.status_code 9 | assert_equal "true", page.response_headers["X-Before-Action"] 10 | assert_equal "true", page.response_headers["X-After-Action"] 11 | assert_equal "true", page.response_headers["X-Before-Around-Action"] 12 | assert_equal "true", page.response_headers["X-After-Around-Action"] 13 | end 14 | 15 | test "runs callbacks for specific actions" do 16 | visit "/create-account" 17 | assert_equal "true", page.response_headers["X-Before-Specific-Action"] 18 | 19 | visit "/login" 20 | assert_nil page.response_headers["X-Before-Specific-Action"] 21 | end 22 | 23 | test "handles rendering in callback chain" do 24 | visit "/login?early_return=true&fail=true" 25 | 26 | assert_equal "early return", page.html 27 | assert_equal 201, page.status_code 28 | assert_equal "true", page.response_headers["X-Before-Action"] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/integration/configurations_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ConfigurationsTest < IntegrationTest 4 | test "multiple configurations" do 5 | visit "/secondary" 6 | 7 | assert_equal current_path, "/admin/login" 8 | end 9 | 10 | test "prefix with custom Roda router" do 11 | visit "/admin/custom1" 12 | assert_equal "Custom admin route", page.html 13 | end 14 | 15 | test "prefix with custom Rails route" do 16 | visit "/admin/custom2" 17 | assert_equal "Custom admin route", page.html 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/integration/constraint_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ConstraintTest < IntegrationTest 4 | test "authenticated constraint" do 5 | visit "/authenticated" 6 | assert_equal "/login", page.current_path 7 | assert_match "Please login to continue", page.find("#alert").text 8 | 9 | register 10 | visit "/authenticated" 11 | assert_equal "/authenticated", page.current_path 12 | assert_includes page.html, %(Authenticated as user@example.com) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/integration/controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ControllerTest < IntegrationTest 4 | test "defines #rodauth method on ActionController::API" do 5 | assert ActionController::API.method_defined?(:rodauth) 6 | end 7 | 8 | test "current account" do 9 | register(login: "user@example.com", verify: true) 10 | assert_includes page.text, "Authenticated as user@example.com" 11 | end 12 | 13 | test "executing controller methods" do 14 | visit "/" 15 | 16 | assert_match "controller method", page.text 17 | end 18 | 19 | test "rodauth response" do 20 | register 21 | logout 22 | 23 | page.driver.browser.get "/sign_in" 24 | 25 | assert_equal 302, page.status_code 26 | assert_equal "/", page.response_headers["Location"] 27 | assert_equal "true", page.response_headers["X-After"] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/integration/csrf_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CsrfTest < IntegrationTest 4 | test "built-in templates include CSRF token" do 5 | visit "/login" 6 | 7 | assert_match %r(), page.html 8 | end 9 | 10 | test "custom templates include CSRF token" do 11 | visit "/reset-password-request" 12 | 13 | assert_match %r(), page.html 14 | end 15 | 16 | test "rodauth actions verify CSRF token" do 17 | assert_raises ActionController::InvalidAuthenticityToken do 18 | page.driver.browser.post "/login", params: { 19 | "login" => "user@example.com", 20 | "password" => "secret" 21 | } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/integration/email_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class EmailTest < IntegrationTest 4 | test "mailer delivery" do 5 | register(login: "user@example.com") 6 | 7 | assert_equal 1, ActionMailer::Base.deliveries.count 8 | 9 | email = ActionMailer::Base.deliveries[0] 10 | 11 | assert_equal "user@example.com", email[:to].to_s 12 | assert_equal "noreply@rodauth.test", email[:from].to_s 13 | assert_equal "[RodauthTest] Verify Account", email[:subject].to_s 14 | 15 | assert_includes email.body.to_s, "Someone has created an account with this email address" 16 | end 17 | 18 | test "verify login change email" do 19 | register(login: "user@example.com", password: "secret", verify: true) 20 | 21 | visit "/change-login" 22 | 23 | fill_in "Login", with: "new@example.com" 24 | fill_in "Password", with: "secret" 25 | click_on "Change Login" 26 | 27 | assert_equal 2, ActionMailer::Base.deliveries.count 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/integration/flash_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class FlashTest < IntegrationTest 4 | test "redirect notice" do 5 | register 6 | 7 | assert_includes page.html, %(

An email has been sent to you with a link to verify your account

) 8 | refute_includes page.html, %(id="alert") 9 | 10 | visit "/" 11 | 12 | refute_includes page.html, %(id="alert") 13 | refute_includes page.html, %(id="notice") 14 | end 15 | 16 | test "redirect alert" do 17 | visit "/auth1" 18 | 19 | assert_includes page.html, %(

Please login to continue

) 20 | refute_includes page.html, %(id="notice") 21 | 22 | visit "/login" 23 | 24 | refute_includes page.html, %(id="alert") 25 | refute_includes page.html, %(id="notice") 26 | end 27 | 28 | test "now alert" do 29 | login 30 | 31 | assert_includes page.html, %(

There was an error logging in

) 32 | refute_includes page.html, %(id="notice") 33 | 34 | visit "/login" 35 | 36 | refute_includes page.html, %(id="alert") 37 | refute_includes page.html, %(id="notice") 38 | end 39 | 40 | test "halted now alert" do 41 | register(verify: true) 42 | logout 43 | 44 | 4.times { login(password: "invalid") } 45 | 46 | assert_includes page.html, %(id="unlock-account-request-form") 47 | assert_includes page.html, %(

This account is currently locked out and cannot be logged in to

) 48 | 49 | visit "/" 50 | 51 | refute_includes page.html, %(id="alert") 52 | refute_includes page.html, %(id="notice") 53 | end 54 | 55 | test "controller redirect alert" do 56 | visit "/auth2" 57 | 58 | assert_includes page.html, %(

Please login to continue

) 59 | refute_includes page.html, %(id="notice") 60 | 61 | visit "/login" 62 | 63 | refute_includes page.html, %(id="alert") 64 | refute_includes page.html, %(id="notice") 65 | end 66 | 67 | test "preserving flash on double redirect" do 68 | register(password: "secret", verify: true) 69 | 70 | visit "/multifactor-manage" 71 | fill_in "Password", with: "secret" 72 | click_on "View Authentication Recovery Codes" 73 | fill_in "Password", with: "secret" 74 | click_on "Add Authentication Recovery Codes" 75 | assert_text "Additional authentication recovery codes have been added" 76 | 77 | logout 78 | login 79 | 80 | visit "/auth2" 81 | assert_equal "/recovery-auth", page.current_path 82 | assert_text "You need to authenticate via an additional factor before continuing" 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/integration/headers_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class HeadersTest < IntegrationTest 4 | test "included Action Dispatch default headers in response" do 5 | visit "/login" 6 | 7 | Rails.configuration.action_dispatch.default_headers.each do |key, value| 8 | assert_equal value, page.response_headers[key] 9 | end 10 | end 11 | 12 | test "extending remember cookie deadline" do 13 | register(login: "user@example.com") 14 | login # remember login 15 | 16 | cookies = page.driver.browser.rack_mock_session.cookie_jar 17 | 18 | assert cookies["_remember"] # remember cookie was set 19 | 20 | cookies.delete("_rails_app_session") # simulate session expiring 21 | 22 | visit "/" # autologin from remember cookie 23 | 24 | assert_match "Authenticated as user@example.com", page.text 25 | assert_match "_remember", Array(page.response_headers["Set-Cookie"]).join(";") # remember deadline extended 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/integration/instrumentation_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class InstrumentationTest < IntegrationTest 4 | test "logs processing and completion of a request" do 5 | logged = capture_log do 6 | visit "/login" 7 | end 8 | 9 | assert_match(/Processing by RodauthController#login as HTML/, logged) 10 | refute_match(/Parameters/, logged) 11 | if ::Rails.gem_version >= Gem::Version.new("6.0") 12 | assert_match(/Completed 200 OK in \d+ms \(Views: \d+ms | ActiveRecord: \d+\.\d+ms | Allocations: \d+\)/, logged) 13 | else 14 | assert_match(/Completed 200 OK in \d+ms \(Views: \d+ms | ActiveRecord: \d+\.\d+ms\)/, logged) 15 | end 16 | end 17 | 18 | test "logs JSON requests" do 19 | logged = capture_log do 20 | page.driver.browser.post "/json/login", 21 | { "login" => "user@example.com", "password" => "secret" }.to_json, 22 | { "CONTENT_TYPE" => "application/json", "HTTP_ACCEPT" => "application/json" } 23 | end 24 | 25 | assert_match(/Processing by ActionController::API#login as JSON/, logged) 26 | assert_match(/Parameters: {"login" ?=> ?"user@example\.com", "password" ?=> ?"secret"}/, logged) 27 | assert_match(/Completed 401 Unauthorized/, logged) 28 | end 29 | 30 | test "logs redirects" do 31 | logged = capture_log do 32 | visit "/change-password" 33 | end 34 | 35 | assert_match(/Processing by RodauthController#change_password as HTML/, logged) 36 | assert_match(/Redirected to \/login/, logged) 37 | assert_match(/Completed 302 Found/, logged) 38 | end 39 | 40 | test "handles early response via callback" do 41 | logged = capture_log do 42 | visit "/login?early_return=true" 43 | end 44 | 45 | assert_match(/Processing by RodauthController#login as HTML/, logged) 46 | assert_match(/Completed 201 Created in \d+ms/, logged) 47 | end 48 | 49 | test "logs response status when redirecting inside controller" do 50 | logged = capture_log do 51 | visit "/auth2" 52 | end 53 | 54 | assert_match(/Completed 302 Found/, logged) 55 | end 56 | 57 | test "logs response status when halting inside controller" do 58 | logged = capture_log do 59 | visit "/auth_json" 60 | end 61 | 62 | assert_match(/Completed 401 Unauthorized/, logged) 63 | end 64 | 65 | private 66 | 67 | def capture_log 68 | io = StringIO.new 69 | original_logger = ActionController::Base.logger 70 | ActionController::Base.logger = Logger.new(io) 71 | yield 72 | ActionController::Base.logger = original_logger 73 | io.string 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/integration/json_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class JsonTest < IntegrationTest 4 | test "works with ActionController::API and JWT" do 5 | page.driver.browser.post "/jwt/create-account", 6 | { "login" => "user@example.com", "password" => "secret", "password-confirm" => "secret" }.to_json, 7 | { "CONTENT_TYPE" => "application/json", "HTTP_ACCEPT" => "application/json" } 8 | 9 | assert_equal %({"success":"An email has been sent to you with a link to verify your account"}), page.html 10 | 11 | email = ActionMailer::Base.deliveries.last 12 | assert_match "Someone has created an account with this email address", email.body.to_s 13 | end 14 | 15 | test "works with ActionController::API and JSON" do 16 | page.driver.browser.post "/json/create-account", 17 | { "login" => "user@example.com", "password" => "secret", "password-confirm" => "secret" }.to_json, 18 | { "CONTENT_TYPE" => "application/json", "HTTP_ACCEPT" => "application/json" } 19 | 20 | assert_equal %({"success":"An email has been sent to you with a link to verify your account"}), page.html 21 | 22 | email = ActionMailer::Base.deliveries.last 23 | assert_match "Someone has created an account with this email address", email.body.to_s 24 | end 25 | 26 | test "works with non-rewindable rack inputs (e.g. Falcon)" do 27 | input = StringIO.new({ "login" => "user@example.com", "password" => "secret", "password-confirm" => "secret" }.to_json) 28 | input.singleton_class.send(:define_method, :rewind) {} # no-op 29 | 30 | page.driver.browser.post "/json/create-account", nil, { 31 | "CONTENT_TYPE" => "application/json", 32 | "HTTP_ACCEPT" => "application/json", 33 | input: input 34 | } 35 | 36 | assert_equal %({"success":"An email has been sent to you with a link to verify your account"}), page.html 37 | 38 | email = ActionMailer::Base.deliveries.last 39 | assert_match "Someone has created an account with this email address", email.body.to_s 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/integration/model_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ModelTest < IntegrationTest 4 | test "allows referencing custom columns for new accounts" do 5 | visit "/create-account" 6 | fill_in "Login", with: "foo@bar.com" 7 | fill_in "Password", with: "supersecret" 8 | fill_in "Confirm Password", with: "supersecret" 9 | assert_nothing_raised { click_on "Create Account" } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/integration/redirect_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RedirectTest < IntegrationTest 4 | test "requiring authentication from Rodauth app" do 5 | visit "/auth1" 6 | assert_equal "/login", current_path 7 | 8 | register 9 | visit "/auth1" 10 | 11 | assert_equal "/auth1", current_path 12 | assert_includes page.html, %(Authenticated as user@example.com) 13 | end 14 | 15 | test "requiring authentication from Rails controller" do 16 | visit "/auth2" 17 | assert_equal "/login", current_path 18 | 19 | register 20 | visit "/auth2" 21 | 22 | assert_equal "/auth2", current_path 23 | assert_includes page.html, %(Authenticated as user@example.com) 24 | end 25 | 26 | test "redirecting with query parameters" do 27 | register 28 | visit "/create-account?foo=bar" 29 | 30 | assert_equal "http://www.example.com/?foo=bar", current_url 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/integration/rescue_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RescueTest < IntegrationTest 4 | test "runs #rescue_from handlers around Rodauth actions" do 5 | visit "/login?raise=true" 6 | 7 | assert_equal "rescued response", page.html 8 | assert_equal 500, page.status_code 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/integration/session_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SessionTest < IntegrationTest 4 | test "resetting session on logout" do 5 | register 6 | 7 | old_session_id = page.find("#session_id").text 8 | 9 | logout 10 | 11 | new_session_id = page.find("#session_id").text 12 | 13 | refute_equal old_session_id, new_session_id 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/internal_request_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class InternalRequestTest < UnitTest 4 | test "inheriting URL options from Action Mailer" do 5 | RodauthApp.rodauth.create_account(login: "user@example.com", password: "secret") 6 | email = ActionMailer::Base.deliveries.last 7 | assert_match %r{https://example\.com}, email.body.to_s 8 | end 9 | 10 | test "overriding URL options directly" do 11 | rodauth = RodauthApp.rodauth.allocate 12 | rodauth.rails_url_options[:host] = "sub.example.com" 13 | 14 | assert_equal "https://sub.example.com", rodauth.base_url 15 | assert_equal "example.com", ActionMailer::Base.default_url_options[:host] 16 | end 17 | 18 | test "overriding URL options via rack env" do 19 | env = { "HTTP_HOST" => "foobar.com", "rack.url_scheme" => "http" } 20 | RodauthApp.rodauth.create_account(login: "user@example.com", password: "secret", env: env) 21 | email = ActionMailer::Base.deliveries.last 22 | assert_match %r{http://foobar\.com}, email.body.to_s 23 | end 24 | 25 | test "missing Action Mailer default URL options" do 26 | ActionMailer::Base.default_url_options = {} 27 | assert_equal "/create-account", RodauthApp.rodauth.create_account_path 28 | assert_raises Rodauth::Rails::Error do 29 | RodauthApp.rodauth.create_account_url 30 | end 31 | assert_raises Rodauth::Rails::Error do 32 | RodauthApp.rodauth.create_account(login: "user@example.com", password: "secret") 33 | end 34 | ensure 35 | ActionMailer::Base.default_url_options = { host: "example.com", protocol: "https" } 36 | end 37 | 38 | test "internal request eval" do 39 | path = RodauthApp.rodauth.internal_request_eval { login_path } 40 | assert_equal "/login", path 41 | end 42 | 43 | test "skipping callbacks" do 44 | env = { "QUERY_STRING" => "early_return=true" } 45 | RodauthApp.rodauth.create_account(login: "user@example.com", password: "secret", env: env) 46 | assert_equal 1, Account.count 47 | end 48 | 49 | test "skipping rescue handlers" do 50 | params = { "raise" => "true" } 51 | assert_raises NotImplementedError do 52 | RodauthApp.rodauth.create_account(login: "user@example.com", password: "secret", params: params) 53 | end 54 | end 55 | 56 | test "skipping instrumentation" do 57 | events = [] 58 | ActiveSupport::Notifications.subscribe(/action_controller/) { |event| events << event } 59 | RodauthApp.rodauth.create_account(login: "user@example.com", password: "secret") 60 | assert_empty events 61 | end 62 | 63 | test "path class methods" do 64 | assert_equal "https://example.com/create-account", RodauthApp.rodauth.create_account_url 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/model_mixin_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ModelMixinTest < UnitTest 4 | test "module builder method with default configuration" do 5 | account_class = define_account_class 6 | account_class.include Rodauth::Rails.model(association_options: { dependent: nil }) 7 | reflection = account_class.reflect_on_association(:password_reset_key) 8 | assert_nil reflection.options.fetch(:dependent) 9 | end 10 | 11 | test "module builder method with secondary configuration" do 12 | account_class = define_account_class 13 | account_class.include Rodauth::Rails.model(:json, association_options: { dependent: nil }) 14 | reflection = account_class.reflect_on_association(:verification_key) 15 | assert_nil reflection.options.fetch(:dependent) 16 | refute account_class.reflect_on_association(:password_reset_key) 17 | end 18 | 19 | test "unknown configuration" do 20 | assert_raises Rodauth::Rails::Error do 21 | Rodauth::Rails.model(:unknown) 22 | end 23 | end 24 | 25 | private 26 | 27 | def define_account_class 28 | account_class = Class.new(ActiveRecord::Base) 29 | account_class.table_name = :accounts 30 | account_class 31 | end 32 | 33 | def teardown 34 | ActiveSupport::Dependencies.clear # clear cache used for :class_name association option 35 | super 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/rails_app/Rakefile: -------------------------------------------------------------------------------- 1 | require_relative "config/application" 2 | 3 | Rails.application.load_tasks 4 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/admin/rodauth_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::RodauthController < ApplicationController 2 | def custom 3 | render plain: "Custom admin route" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | 4 | private 5 | 6 | def current_account 7 | rodauth.rails_account 8 | end 9 | helper_method :current_account 10 | end 11 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/rodauth_controller.rb: -------------------------------------------------------------------------------- 1 | class RodauthController < ApplicationController 2 | before_action :before_route 3 | after_action :after_route 4 | around_action :around_route 5 | before_action :before_specific_route, only: [:create_account] 6 | 7 | rescue_from NotImplementedError do 8 | render plain: "rescued response", status: 500 9 | end 10 | 11 | private 12 | 13 | def before_route 14 | response.headers["X-Before-Action"] = "true" 15 | 16 | if params[:early_return] 17 | render plain: "early return", status: 201 18 | end 19 | end 20 | 21 | def after_route 22 | response.headers["X-After-Action"] = "true" 23 | end 24 | 25 | def around_route 26 | response.headers["X-Before-Around-Action"] = "true" 27 | yield 28 | response.headers["X-After-Around-Action"] = "true" 29 | end 30 | 31 | def before_specific_route 32 | response.header["X-Before-Specific-Action"] = "true" 33 | end 34 | 35 | def some_method 36 | "controller method" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/test_controller.rb: -------------------------------------------------------------------------------- 1 | class TestController < ApplicationController 2 | def root 3 | render :template 4 | end 5 | 6 | def auth1 7 | render :template 8 | end 9 | 10 | def auth2 11 | rodauth.require_account 12 | 13 | render :template 14 | end 15 | 16 | def basic_auth 17 | respond_to do |format| 18 | format.text { render plain: "Basic Auth" } 19 | format.json { render json: { message: "Basic Auth" } } 20 | end 21 | end 22 | 23 | def secondary 24 | rodauth(:admin).require_authentication 25 | 26 | render :template 27 | end 28 | 29 | def auth_json 30 | rodauth(:json).require_authentication 31 | 32 | head :ok 33 | end 34 | 35 | def sign_in 36 | rodauth.account_from_login(Account.first.email) 37 | 38 | rodauth_response do 39 | rodauth.login("test") 40 | end 41 | 42 | headers["X-After"] = "true" 43 | end 44 | 45 | def roda 46 | render json: { instance: rodauth.scope.inspect, class: rodauth.scope.class.inspect } 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/rails_app/app/misc/rodauth_admin.rb: -------------------------------------------------------------------------------- 1 | class RodauthAdmin < Rodauth::Rails::Auth 2 | configure do 3 | enable :login, :two_factor_base 4 | enable :webauthn_autofill, :webauthn_modify_email unless RUBY_ENGINE == "jruby" 5 | prefix "/admin" 6 | rails_controller { Admin::RodauthController } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/rails_app/app/misc/rodauth_app.rb: -------------------------------------------------------------------------------- 1 | class RodauthApp < Rodauth::Rails::App 2 | configure RodauthMain 3 | configure RodauthAdmin, :admin 4 | 5 | configure(:jwt) do 6 | enable :jwt, :create_account, :verify_account 7 | rails_controller { ActionController::API } 8 | only_json? true 9 | prefix "/jwt" 10 | jwt_secret "secret" 11 | account_status_column :status 12 | end 13 | 14 | configure(:json) do 15 | enable :json, :create_account, :verify_account, :two_factor_base 16 | rails_controller { ActionController::API } 17 | only_json? true 18 | prefix "/json" 19 | account_status_column :status 20 | end 21 | 22 | route do |r| 23 | rodauth.load_memory 24 | 25 | r.rodauth 26 | r.rodauth(:admin) 27 | r.on("jwt") { r.rodauth(:jwt) } 28 | r.on("json") { r.rodauth(:json) } 29 | 30 | r.on("assets") { "" } 31 | r.get("admin/custom1") { "Custom admin route" } 32 | 33 | if r.path == rails_routes.auth1_path 34 | rodauth.require_account 35 | end 36 | if r.path.start_with?(rails_routes.basic_auth_path) 37 | rodauth.require_http_basic_auth 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/rails_app/app/misc/rodauth_main.rb: -------------------------------------------------------------------------------- 1 | class RodauthMain < Rodauth::Rails::Auth 2 | configure do 3 | enable :create_account, :verify_account, :verify_account_grace_period, 4 | :login, :remember, :logout, :active_sessions, :http_basic_auth, 5 | :reset_password, :change_password, :change_login, :verify_login_change, 6 | :close_account, :lockout, :recovery_codes, :internal_request, 7 | :path_class_methods 8 | 9 | rails_controller { RodauthController } 10 | 11 | before_rodauth do 12 | if param_or_nil("raise") 13 | raise NotImplementedError 14 | elsif param_or_nil("fail") 15 | fail "failed" 16 | end 17 | end 18 | 19 | account_status_column :status 20 | 21 | email_subject_prefix "[RodauthTest] " 22 | email_from "noreply@rodauth.test" 23 | 24 | require_login_confirmation? false 25 | verify_account_set_password? false 26 | extend_remember_deadline? true 27 | max_invalid_logins 3 28 | 29 | if defined?(::Turbo) 30 | after_login_failure do 31 | if rails_request.format.turbo_stream? 32 | return_response rails_render(turbo_stream: [turbo_stream.append("login-form", %(
login failed
))]) 33 | end 34 | end 35 | check_csrf? { rails_request.format.turbo_stream? ? false : super() } 36 | end 37 | 38 | after_login { remember_login } 39 | before_create_account { rails_account.username } 40 | 41 | already_logged_in do 42 | if rails_request.query_parameters.any? 43 | redirect rails_routes.root_path(rails_request.query_parameters) 44 | end 45 | end 46 | 47 | logout_redirect { rails_routes.root_path } 48 | verify_account_redirect { login_redirect } 49 | reset_password_redirect { login_path } 50 | title_instance_variable :@page_title 51 | 52 | verify_login_change_route nil 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/rails_app/app/models/account.rb: -------------------------------------------------------------------------------- 1 | class Account < ApplicationRecord 2 | include Rodauth::Rails.model 3 | if ActiveRecord.version >= Gem::Version.new("7.0") 4 | enum :status, { unverified: 1, verified: 2, closed: 3 } 5 | else 6 | enum status: { unverified: 1, verified: 2, closed: 3 } 7 | end 8 | validates_presence_of :email 9 | end 10 | -------------------------------------------------------------------------------- /test/rails_app/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /test/rails_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= @page_title || "Rodauth::Rails Test" %> 5 | 6 | <%= csrf_meta_tags %> 7 | 8 | 9 | 10 | <% if notice %> 11 |

<%= notice %>

12 | <% end %> 13 | 14 | <% if alert %> 15 |

<%= alert %>

16 | <% end %> 17 | 18 | <%= yield %> 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/rails_app/app/views/rodauth/_global_logout_field.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= check_box_tag rodauth.global_logout_param, "t", false, id: "custom-global-logout", class: "form-check-input" %> 4 | <%= label_tag "global-logout", "Logout all Logged In Sessons?", class: "form-check-label" %> 5 |
6 |
7 | -------------------------------------------------------------------------------- /test/rails_app/app/views/rodauth/close_account.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag rodauth.close_account_path, method: :post do %> 2 | <%= rodauth.render "password-field" if rodauth.close_account_requires_password? %> 3 | <%= rodauth.button rodauth.close_account_button, class: "btn btn-danger" %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /test/rails_app/app/views/rodauth/reset_password_request.html.erb: -------------------------------------------------------------------------------- 1 | <% root_path # test that Rails routes still work %> 2 | 3 | <%= form_tag rodauth.reset_password_request_path, method: :post, id: "custom-reset-password-request-form" do %> 4 | <% if params[rodauth.login_param] && !rodauth.field_error(rodauth.login_param) %> 5 | <%= hidden_field_tag rodauth.login_param, params[rodauth.login_param] %> 6 | <% else %> 7 | <%= email_field_tag rodauth.login_param, params[rodauth.login_param] %> 8 | <% end %> 9 | <%= submit_tag "Request Password Reset" %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /test/rails_app/app/views/rodauth/verify_account.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag rodauth.verify_account_path, method: :post, id: "custom-verify-account-form" do %> 2 | <%= submit_tag "Verify Account" %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /test/rails_app/app/views/test/template.html.erb: -------------------------------------------------------------------------------- 1 | <% if rodauth.authenticated? %> 2 |

Authenticated as <%= current_account.email %>

3 | <% else %> 4 |

Not authenticated

5 | <% end %> 6 | 7 |

<%= rodauth.rails_controller_eval { some_method } %>

8 | 9 | <%= session.id %>

10 | -------------------------------------------------------------------------------- /test/rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require "rails/version" 2 | require "logger" if Rails.gem_version >= Gem::Version.new("6.0") && Rails.gem_version < Gem::Version.new("7.1") 3 | require "active_record/railtie" 4 | require "action_controller/railtie" 5 | require "action_mailer/railtie" 6 | require "action_cable/engine" 7 | require "active_job/railtie" 8 | require "rails/test_unit/railtie" 9 | 10 | require "rodauth-rails" 11 | begin 12 | require "turbo-rails" 13 | rescue LoadError 14 | end 15 | 16 | module RailsApp 17 | class Application < Rails::Application 18 | config.root = Pathname("#{__dir__}/..").expand_path 19 | config.secret_key_base = "a8457c8003e83577e92708bd56e19bdc4442c689f458f483a30e580611c578a3" 20 | config.logger = Logger.new(nil) 21 | config.eager_load = true 22 | config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" 23 | config.action_dispatch.show_exceptions = Rails.gem_version >= Gem::Version.new("7.1") ? :none : false 24 | config.action_mailer.delivery_method = :test 25 | config.action_mailer.default_url_options = { host: "example.com", protocol: "https" } 26 | config.active_record.maintain_test_schema = false 27 | config.active_record.legacy_connection_handling = false if ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR == 0 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/rails_app/config/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: sqlite3 3 | database: ":memory:" 4 | -------------------------------------------------------------------------------- /test/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | require_relative "application" 2 | 3 | RailsApp::Application.initialize! 4 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/rodauth.rb: -------------------------------------------------------------------------------- 1 | Rodauth::Rails.configure do |config| 2 | config.app = "RodauthApp" 3 | end 4 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/sequel.rb: -------------------------------------------------------------------------------- 1 | require "sequel/core" 2 | 3 | DB = Sequel.connect("#{"jdbc:" if RUBY_ENGINE == "jruby"}sqlite://", extensions: :activerecord_connection) 4 | -------------------------------------------------------------------------------- /test/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: "test#root" 3 | 4 | controller :test do 5 | get :auth1 6 | get :auth2 7 | get :basic_auth 8 | get :secondary 9 | get :auth_json 10 | get :sign_in 11 | get :roda 12 | end 13 | 14 | get "/admin/custom2" => "admin/rodauth#custom" 15 | 16 | constraints Rodauth::Rails.authenticate do 17 | get "/authenticated" => "test#root" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | 3 | require "warning" 4 | Gem.path.each { |path| Warning.ignore(//, path) } # ignore warnings in dependencies 5 | 6 | require "bundler/setup" 7 | require "i18n/backend" 8 | require_relative "rails_app/config/environment" 9 | require "rails/test_help" 10 | require "capybara/rails" 11 | 12 | puts "Rails #{Rails.version}" if ENV["CI"] 13 | 14 | ActiveRecord::Migrator.migrations_paths = [Rails.root.join("db/migrate")] 15 | Rails.backtrace_cleaner.remove_silencers! # show full stack traces 16 | 17 | module TestSetupTeardown 18 | def setup 19 | super 20 | if ActiveRecord.version >= Gem::Version.new("7.2") 21 | ActiveRecord::Base.connection_pool.migration_context.up 22 | elsif ActiveRecord.version >= Gem::Version.new("5.2") 23 | ActiveRecord::Base.connection.migration_context.up 24 | else 25 | ActiveRecord::Migrator.up(Rails.application.paths["db/migrate"].to_a) 26 | end 27 | end 28 | 29 | def teardown 30 | super 31 | if ActiveRecord.version >= Gem::Version.new("7.2") 32 | ActiveRecord::Base.connection_pool.migration_context.up 33 | elsif ActiveRecord.version >= Gem::Version.new("5.2") 34 | ActiveRecord::Base.connection.migration_context.down 35 | else 36 | ActiveRecord::Migrator.down(Rails.application.paths["db/migrate"].to_a) 37 | end 38 | ActiveRecord::Base.clear_cache! # clear schema cache 39 | ActionMailer::Base.deliveries.clear 40 | end 41 | end 42 | 43 | class UnitTest < ActiveSupport::TestCase 44 | self.test_order = :random 45 | include TestSetupTeardown 46 | end 47 | 48 | class IntegrationTest < UnitTest 49 | include Capybara::DSL 50 | 51 | def register(login: "user@example.com", password: "secret", verify: false) 52 | visit "/create-account" 53 | fill_in "Login", with: login 54 | fill_in "Password", with: password 55 | fill_in "Confirm Password", with: password 56 | click_on "Create Account" 57 | 58 | if verify 59 | email = ActionMailer::Base.deliveries.last 60 | verify_account_link = email.body.to_s[%r{/verify-account\S+}] 61 | 62 | visit verify_account_link 63 | click_on "Verify Account" 64 | end 65 | end 66 | 67 | def login(login: "user@example.com", password: "secret") 68 | visit "/login" 69 | fill_in "Login", with: login 70 | fill_in "Password", with: password 71 | click_on "Login" 72 | end 73 | 74 | def logout 75 | visit "/logout" 76 | click_on "Logout" 77 | end 78 | 79 | def teardown 80 | super 81 | Capybara.reset_sessions! 82 | end 83 | end 84 | --------------------------------------------------------------------------------