From 3f8213409b063f598f19e46e9a576653d859cde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janko=20Marohni=C4=87?= Date: Mon, 20 Nov 2023 11:26:47 +0100 Subject: [PATCH 1/5] Add native route helpers --- README.md | 76 +++++++++++-------- lib/generators/rodauth/install_generator.rb | 4 + lib/rodauth/rails/app.rb | 6 -- lib/rodauth/rails/feature/base.rb | 13 ++++ lib/rodauth/rails/feature/instrumentation.rb | 4 +- lib/rodauth/rails/railtie.rb | 9 ++- lib/rodauth/rails/routing.rb | 44 +++++++++++ lib/rodauth/rails/tasks.rake | 12 --- lib/rodauth/rails/tasks/routes.rb | 70 ----------------- test/generators/install_generator_test.rb | 12 +++ test/integration/email_test.rb | 2 +- test/integration/instrumentation_test.rb | 8 +- test/integration/routes_test.rb | 28 +++++++ test/rails_app/app/misc/rodauth_main.rb | 1 + .../rodauth/reset_password_request.html.erb | 1 + test/rails_app/config/routes.rb | 3 + test/rake_test.rb | 62 --------------- test/test_helper.rb | 8 +- 18 files changed, 167 insertions(+), 196 deletions(-) create mode 100644 lib/rodauth/rails/routing.rb delete mode 100644 lib/rodauth/rails/tasks.rake delete mode 100644 lib/rodauth/rails/tasks/routes.rb create mode 100644 test/integration/routes_test.rb delete mode 100644 test/rake_test.rb diff --git a/README.md b/README.md index 24f9f2f0..3cfa8845 100644 --- a/README.md +++ b/README.md @@ -124,39 +124,44 @@ $ rails middleware ### Routes -Because requests to Rodauth endpoints are handled by Roda, Rodauth routes will -not show in `rails routes`. You can use the `rodauth:routes` rake task to view -the list of endpoints based on currently loaded features: +The Rodauth object defines `*_path` and `*_url` route helpers, but you can +also expose them as Rails URL helpers, which are accessible everywhere. +```rb +# config/routes.rb +Rails.application.routes.draw do + # generate URL helpers from your Rodauth configuration + rodauth + # rodauth(as: :user) - prefix URL helpers with "user_*" +end +``` ```sh -$ rails rodauth:routes +$ rails routes -g rodauth ``` ``` -Routes handled by RodauthApp: - - GET|POST /login rodauth.login_path - GET|POST /create-account rodauth.create_account_path - GET|POST /verify-account-resend rodauth.verify_account_resend_path - GET|POST /verify-account rodauth.verify_account_path - GET|POST /change-password rodauth.change_password_path - GET|POST /change-login rodauth.change_login_path - GET|POST /logout rodauth.logout_path - GET|POST /remember rodauth.remember_path - GET|POST /reset-password-request rodauth.reset_password_request_path - GET|POST /reset-password rodauth.reset_password_path - GET|POST /verify-login-change rodauth.verify_login_change_path - GET|POST /close-account rodauth.close_account_path + Prefix Verb URI Pattern Controller#Action + login GET|POST /login(.:format) rodauth#login + create_account GET|POST /create-account(.:format) rodauth#create_account + verify_account_resend GET|POST /verify-account-resend(.:format) rodauth#verify_account_resend + verify_account GET|POST /verify-account(.:format) rodauth#verify_account + logout GET|POST /logout(.:format) rodauth#logout + remember GET|POST /remember(.:format) rodauth#remember +reset_password_request GET|POST /reset-password-request(.:format) rodauth#reset_password_request + reset_password GET|POST /reset-password(.:format) rodauth#reset_password + change_password GET|POST /change-password(.:format) rodauth#change_password + change_login GET|POST /change-login(.:format) rodauth#change_login + verify_login_change GET|POST /verify-login-change(.:format) rodauth#verify_login_change + close_account GET|POST /close-account(.:format) rodauth#close_account ``` -Using this information, you can add some basic authentication links to your -navigation header: +We can use these URL helpers to add some basic authentication links to our app: ```erb <% if rodauth.logged_in? %> - <%= link_to "Sign out", rodauth.logout_path, data: { turbo_method: :post } %> + <%= button_to "Sign out", logout_path, method: :post %> <% else %> - <%= link_to "Sign in", rodauth.login_path %> - <%= link_to "Sign up", rodauth.create_account_path %> + <%= link_to "Sign in", login_path %> + <%= link_to "Sign up", create_account_path %> <% end %> ``` @@ -480,12 +485,23 @@ end class Admin::RodauthController < ApplicationController end ``` +```rb +# config/routes.rb +Rails.application.routes.draw do + rodauth + rodauth(:admin) # generate URL helpers from the secondary configuration +end +``` -Then in your application you can reference the secondary Rodauth instance: +Then in your application you can reference the secondary Rodauth instance and URL helpers: ```rb rodauth(:admin).authenticated? # checks "admin_account_id" session value -rodauth(:admin).login_path #=> "/admin/login" +rodauth(:admin).rails_account # returns authenticated admin account + +# URL helpers are prefixed with the configuration name +admin_login_path #=> "/admin/login" +admin_create_account_url #=> "https://example.com/admin/create-account" ``` You'll likely want to save the information of which account belongs to which @@ -494,16 +510,11 @@ that. Note that you can also [share configuration via inheritance][inheritance]. ## Outside of a request -The [internal_request] and [path_class_methods] features are supported, with defaults taken from `config.action_mailer.default_url_options`. +The [internal_request] feature is supported, with defaults taken from `config.action_mailer.default_url_options`. ```rb -# internal requests RodauthApp.rodauth.create_account(login: "user@example.com", password: "secret123") RodauthApp.rodauth(:admin).verify_account(account_login: "admin@example.com") - -# path and URL methods -RodauthApp.rodauth.close_account_path #=> "/close-account" -RodauthApp.rodauth(:admin).otp_setup_url #=> "http://localhost:3000/admin/otp-setup" ``` ### Calling instance methods @@ -774,8 +785,7 @@ conduct](CODE_OF_CONDUCT.md). [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html [simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator -[internal_request]: http://rodauth.jeremyevans.net/rdoc/files/doc/internal_request_rdoc.html -[path_class_methods]: https://rodauth.jeremyevans.net/rdoc/files/doc/path_class_methods_rdoc.html +[internal_request]: http://rodauth.jeremyevans.net/rdoc/files/doc/internal_request_rdoc.htmll [account types]: https://github.com/janko/rodauth-rails/wiki/Account-Types [custom mailer worker]: https://github.com/janko/rodauth-rails/wiki/Custom-Mailer-Worker [Turbo]: https://turbo.hotwired.dev/ diff --git a/lib/generators/rodauth/install_generator.rb b/lib/generators/rodauth/install_generator.rb index 4ac9e589..b0de90a8 100644 --- a/lib/generators/rodauth/install_generator.rb +++ b/lib/generators/rodauth/install_generator.rb @@ -65,6 +65,10 @@ def create_mailer end end + def define_routes + route "rodauth" + end + def create_fixtures generator_options = ::Rails.configuration.generators.options if generator_options[:test_unit][:fixture] && generator_options[:test_unit][:fixture_replacement].nil? diff --git a/lib/rodauth/rails/app.rb b/lib/rodauth/rails/app.rb index 05ac3d57..d1bdad87 100644 --- a/lib/rodauth/rails/app.rb +++ b/lib/rodauth/rails/app.rb @@ -66,12 +66,6 @@ def self.rodauth!(name) rodauth(name) or fail ArgumentError, "unknown rodauth configuration: #{name.inspect}" end - # The newrelic_rpm gem expects this when we pass the roda class as - # :controller in instrumentation payload. - def self.controller_path - name.underscore - end - module RequestMethods # Automatically route the prefix if it hasn't been routed already. This # way people only have to update prefix in their Rodauth configurations. diff --git a/lib/rodauth/rails/feature/base.rb b/lib/rodauth/rails/feature/base.rb index e137ec10..a8bcba59 100644 --- a/lib/rodauth/rails/feature/base.rb +++ b/lib/rodauth/rails/feature/base.rb @@ -57,6 +57,19 @@ def session private + def before_rodauth + rails_request.path_parameters.merge!(rails_path_parameters) unless internal_request? + super + end + + def rails_path_parameters + controller = [rails_controller.module_parent_name&.underscore, rails_controller.controller_name].compact.join("/") + route_method = self.class.route_hash.fetch(request.path.sub(/^#{prefix}/, "")) + action = route_method.to_s.sub(/^handle_/, "") + + { controller: controller, action: action } + end + def instantiate_rails_account if defined?(ActiveRecord::Base) && rails_account_model < ActiveRecord::Base rails_account_model.instantiate(account.stringify_keys) diff --git a/lib/rodauth/rails/feature/instrumentation.rb b/lib/rodauth/rails/feature/instrumentation.rb index c7b989ca..49331c48 100644 --- a/lib/rodauth/rails/feature/instrumentation.rb +++ b/lib/rodauth/rails/feature/instrumentation.rb @@ -34,8 +34,8 @@ def rails_instrument_request request = rails_request raw_payload = { - controller: self.class.roda_class.name, - action: "call", + controller: rails_controller.name, + action: rails_path_parameters[:action], request: request, params: request.filtered_parameters, headers: request.headers, diff --git a/lib/rodauth/rails/railtie.rb b/lib/rodauth/rails/railtie.rb index cc68de60..bdea93d5 100644 --- a/lib/rodauth/rails/railtie.rb +++ b/lib/rodauth/rails/railtie.rb @@ -1,6 +1,7 @@ require "rodauth/rails/middleware" require "rodauth/rails/controller_methods" require "rodauth/rails/test" +require "rodauth/rails/routing" require "rails" @@ -19,6 +20,10 @@ class Railtie < ::Rails::Railtie end end + initializer "rodauth.routing" do + ActionDispatch::Routing::Mapper.include Rodauth::Rails::Routing + end + initializer "rodauth.test" do # Rodauth uses RACK_ENV to set the default bcrypt hash cost ENV["RACK_ENV"] = "test" if ::Rails.env.test? @@ -27,10 +32,6 @@ class Railtie < ::Rails::Railtie include Rodauth::Rails::Test::Controller end end - - rake_tasks do - load "rodauth/rails/tasks.rake" - end end end end diff --git a/lib/rodauth/rails/routing.rb b/lib/rodauth/rails/routing.rb new file mode 100644 index 00000000..0a501b09 --- /dev/null +++ b/lib/rodauth/rails/routing.rb @@ -0,0 +1,44 @@ +module Rodauth + module Rails + module Routing + def rodauth(name = nil, as: name) + auth_class = Rodauth::Rails.app.rodauth!(name) + scope = auth_class.roda_class.new({}) + rodauth = auth_class.new(scope) + + controller = rodauth.rails_controller.controller_name + namespace = rodauth.rails_controller.module_parent_name&.underscore + + scope controller: controller, module: namespace, as: as do + auth_class.route_hash.each do |route_path, route_method| + next if route_method.to_s.end_with?("_js") + + path = "#{rodauth.prefix}/#{route_path}" + action = route_method.to_s.sub(/\Ahandle_/, "") + verbs = rodauth_verbs(rodauth, route_method) + + match path, action: action, as: action, via: verbs + end + end + end + + private + + def rodauth_verbs(rodauth, route_method) + file_path, start_line = rodauth.method(:"_#{route_method}").source_location + lines = File.foreach(file_path).to_a + indentation = lines[start_line - 1][/^\s+/] + verbs = [] + + lines[start_line..-1].each do |code| + verbs << :GET if code.include?("r.get") && !rodauth.only_json? + verbs << :POST if code.include?("r.post") + break if code.start_with?("#{indentation}end") + end + + verbs << :POST if rodauth.features.include?(:json) && route_method.to_s.match?(/two_factor_(manage|auth)$/) + verbs + end + end + end +end diff --git a/lib/rodauth/rails/tasks.rake b/lib/rodauth/rails/tasks.rake deleted file mode 100644 index 58c5012e..00000000 --- a/lib/rodauth/rails/tasks.rake +++ /dev/null @@ -1,12 +0,0 @@ -require "rodauth/rails/tasks/routes" - -namespace :rodauth do - desc "Lists endpoints that will be routed by your Rodauth app" - task routes: :environment do - puts "Routes handled by #{Rodauth::Rails.app}:" - - Rodauth::Rails.app.opts[:rodauths].each_value do |auth_class| - Rodauth::Rails::Tasks::Routes.new(auth_class).call - end - end -end diff --git a/lib/rodauth/rails/tasks/routes.rb b/lib/rodauth/rails/tasks/routes.rb deleted file mode 100644 index 50abe528..00000000 --- a/lib/rodauth/rails/tasks/routes.rb +++ /dev/null @@ -1,70 +0,0 @@ -module Rodauth - module Rails - module Tasks - class Routes - IGNORE = [:webauthn_setup_js, :webauthn_auth_js, :webauthn_autofill_js] - JSON_POST = [:two_factor_manage, :two_factor_auth] - - attr_reader :auth_class - - def initialize(auth_class) - @auth_class = auth_class - end - - def call - routes = auth_class.route_hash.map do |path, handle_method| - route_name = handle_method.to_s.sub(/\Ahandle_/, "").to_sym - next if IGNORE.include?(route_name) - verbs = route_verbs(route_name) - - [ - verbs.join("|"), - "#{rodauth.prefix}#{path}", - "rodauth#{configuration_name && "(:#{configuration_name})"}.#{route_name}_path", - ] - end - - routes.compact! - padding = routes.transpose.map { |string| string.map(&:length).max } - - output_lines = routes.map do |columns| - [columns[0].ljust(padding[0]), columns[1].ljust(padding[1]), columns[2]].join(" ") - end - - puts "\n #{output_lines.join("\n ")}" - end - - private - - def route_verbs(route_name) - file_path, start_line = rodauth.method(:"_handle_#{route_name}").source_location - lines = File.foreach(file_path).to_a - indentation = lines[start_line - 1][/^\s+/] - verbs = [] - - lines[start_line..-1].each do |code| - verbs << :GET if code.include?("r.get") && !rodauth.only_json? - verbs << :POST if code.include?("r.post") - break if code.start_with?("#{indentation}end") - end - - verbs << :POST if rodauth.features.include?(:json) && JSON_POST.include?(route_name) - verbs - end - - def rodauth - auth_class.new(scope) - end - - def scope - auth_class.roda_class.new({}) - end - - def configuration_name - auth_class.configuration_name - end - end - end - end -end - diff --git a/test/generators/install_generator_test.rb b/test/generators/install_generator_test.rb index e0e126dd..b81a7f27 100644 --- a/test/generators/install_generator_test.rb +++ b/test/generators/install_generator_test.rb @@ -108,6 +108,12 @@ class InstallGeneratorTest < Rails::Generators::TestCase end end + test "routes" do + run_generator + + assert_file "config/routes.rb", /^ rodauth$/ + end + test "fixtures" do run_generator @@ -129,4 +135,10 @@ class InstallGeneratorTest < Rails::Generators::TestCase prepare_destination end end + + def prepare_destination + super + mkdir "#{destination_root}/config" + File.write "#{destination_root}/config/routes.rb", "Rails.application.routes.draw do\nend" + end end diff --git a/test/integration/email_test.rb b/test/integration/email_test.rb index 362aa138..85d40bb9 100644 --- a/test/integration/email_test.rb +++ b/test/integration/email_test.rb @@ -18,7 +18,7 @@ class EmailTest < IntegrationTest test "verify login change email" do register(login: "user@example.com", password: "secret", verify: true) - visit "/change-login" + visit "/change-email" fill_in "Login", with: "new@example.com" fill_in "Password", with: "secret" diff --git a/test/integration/instrumentation_test.rb b/test/integration/instrumentation_test.rb index de957cf9..118585b3 100644 --- a/test/integration/instrumentation_test.rb +++ b/test/integration/instrumentation_test.rb @@ -6,7 +6,7 @@ class InstrumentationTest < IntegrationTest visit "/login" end - assert_match /Processing by RodauthApp#call as HTML/, logged + assert_match /Processing by RodauthController#login as HTML/, logged refute_match /Parameters/, logged if ::Rails.gem_version >= Gem::Version.new("6.0") assert_match /Completed 200 OK in \d+ms \(Views: \d+ms | ActiveRecord: \d+\.\d+ms | Allocations: \d+\)/, logged @@ -22,7 +22,7 @@ class InstrumentationTest < IntegrationTest { "CONTENT_TYPE" => "application/json", "HTTP_ACCEPT" => "application/json" } end - assert_match /Processing by RodauthApp#call as JSON/, logged + assert_match /Processing by ActionController::API#login as JSON/, logged assert_match /Parameters: {"login"=>"user@example\.com", "password"=>"secret"}/, logged assert_match /Completed 401 Unauthorized/, logged end @@ -32,7 +32,7 @@ class InstrumentationTest < IntegrationTest visit "/change-password" end - assert_match /Processing by RodauthApp#call as HTML/, logged + assert_match /Processing by RodauthController#change_password as HTML/, logged assert_match /Redirected to \/login/, logged assert_match /Completed 302 Found/, logged end @@ -42,7 +42,7 @@ class InstrumentationTest < IntegrationTest visit "/login?early_return=true" end - assert_match /Processing by RodauthApp#call as HTML/, logged + assert_match /Processing by RodauthController#login as HTML/, logged assert_match /Completed 201 Created in \d+ms/, logged end diff --git a/test/integration/routes_test.rb b/test/integration/routes_test.rb new file mode 100644 index 00000000..d5d4d2a7 --- /dev/null +++ b/test/integration/routes_test.rb @@ -0,0 +1,28 @@ +require "test_helper" + +class RoutesTest < IntegrationTest + test "routes for default configuration" do + assert_equal "/login", login_path + assert_equal "http://www.example.com/login", login_url + end + + test "routes for secondary configuration" do + assert_equal "/admin/login", admin_login_path + assert_equal "http://www.example.com/admin/login", admin_login_url + end + + test "changed routes" do + assert_equal "/change-email", change_login_path + assert_equal "http://www.example.com/change-email", change_login_url + end + + test "current url" do + visit "/reset-password-request" + click_on "Croatian" + assert_match %r{/reset-password-request\?locale=hr$}, current_url + end + + test "disabled routes" do + assert_raises(NameError) { verify_login_change_path } + end +end diff --git a/test/rails_app/app/misc/rodauth_main.rb b/test/rails_app/app/misc/rodauth_main.rb index 629a76b9..66961a42 100644 --- a/test/rails_app/app/misc/rodauth_main.rb +++ b/test/rails_app/app/misc/rodauth_main.rb @@ -44,5 +44,6 @@ class RodauthMain < Rodauth::Rails::Auth title_instance_variable :@page_title verify_login_change_route nil + change_login_route "change-email" end end diff --git a/test/rails_app/app/views/rodauth/reset_password_request.html.erb b/test/rails_app/app/views/rodauth/reset_password_request.html.erb index d4418e68..7c998b23 100644 --- a/test/rails_app/app/views/rodauth/reset_password_request.html.erb +++ b/test/rails_app/app/views/rodauth/reset_password_request.html.erb @@ -1,4 +1,5 @@ <% root_path # test that Rails routes still work %> +<%= link_to "Croatian", url_for(locale: "hr") %> <%= form_tag rodauth.reset_password_request_path, method: :post, id: "custom-reset-password-request-form" do %> <% if params[rodauth.login_param] && !rodauth.field_error(rodauth.login_param) %> diff --git a/test/rails_app/config/routes.rb b/test/rails_app/config/routes.rb index 367f079e..bb585232 100644 --- a/test/rails_app/config/routes.rb +++ b/test/rails_app/config/routes.rb @@ -1,6 +1,9 @@ Rails.application.routes.draw do root to: "test#root" + rodauth + rodauth(:admin) + controller :test do get :auth1 get :auth2 diff --git a/test/rake_test.rb b/test/rake_test.rb deleted file mode 100644 index d51d2560..00000000 --- a/test/rake_test.rb +++ /dev/null @@ -1,62 +0,0 @@ -require "test_helper" - -Rails.application.load_tasks - -class RakeTest < ActiveSupport::TestCase - test "rodauth:routes prints routes" do - stdout, _ = capture_io do - Rake::Task["rodauth:routes"].invoke - end - - expected_output = <<~EOS - Routes handled by RodauthApp: - - GET|POST /login rodauth.login_path - GET|POST /create-account rodauth.create_account_path - GET|POST /verify-account-resend rodauth.verify_account_resend_path - GET|POST /verify-account rodauth.verify_account_path - GET|POST /remember rodauth.remember_path - GET|POST /logout rodauth.logout_path - GET|POST /reset-password-request rodauth.reset_password_request_path - GET|POST /reset-password rodauth.reset_password_path - GET|POST /change-password rodauth.change_password_path - GET|POST /change-login rodauth.change_login_path - GET|POST /close-account rodauth.close_account_path - POST /unlock-account-request rodauth.unlock_account_request_path - GET|POST /unlock-account rodauth.unlock_account_path - GET /multifactor-manage rodauth.two_factor_manage_path - GET /multifactor-auth rodauth.two_factor_auth_path - GET|POST /multifactor-disable rodauth.two_factor_disable_path - GET|POST /recovery-auth rodauth.recovery_auth_path - GET|POST /recovery-codes rodauth.recovery_codes_path - - GET|POST /admin/login rodauth(:admin).login_path - GET /admin/multifactor-manage rodauth(:admin).two_factor_manage_path - GET /admin/multifactor-auth rodauth(:admin).two_factor_auth_path - GET|POST /admin/multifactor-disable rodauth(:admin).two_factor_disable_path - GET|POST /admin/webauthn-auth rodauth(:admin).webauthn_auth_path - GET|POST /admin/webauthn-setup rodauth(:admin).webauthn_setup_path - GET|POST /admin/webauthn-remove rodauth(:admin).webauthn_remove_path - POST /admin/webauthn-login rodauth(:admin).webauthn_login_path - - POST /jwt/login rodauth(:jwt).login_path - POST /jwt/create-account rodauth(:jwt).create_account_path - POST /jwt/verify-account-resend rodauth(:jwt).verify_account_resend_path - POST /jwt/verify-account rodauth(:jwt).verify_account_path - - POST /json/login rodauth(:json).login_path - POST /json/create-account rodauth(:json).create_account_path - POST /json/verify-account-resend rodauth(:json).verify_account_resend_path - POST /json/verify-account rodauth(:json).verify_account_path - POST /json/multifactor-manage rodauth(:json).two_factor_manage_path - POST /json/multifactor-auth rodauth(:json).two_factor_auth_path - POST /json/multifactor-disable rodauth(:json).two_factor_disable_path - EOS - - if RUBY_ENGINE == "jruby" - expected_output.gsub!(/^.+webauthn.+$\n/, "") - end - - assert_equal expected_output, stdout - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb index c771e040..d749205b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,6 +12,10 @@ require "rails/test_help" require "capybara/rails" +if ActiveSupport.respond_to?(:test_order) + ActiveSupport.test_order = :random +end + ActiveRecord::Migrator.migrations_paths = [Rails.root.join("db/migrate")] Rails.backtrace_cleaner.remove_silencers! # show full stack traces @@ -38,12 +42,12 @@ def teardown end class UnitTest < ActiveSupport::TestCase - self.test_order = :random include TestSetupTeardown end -class IntegrationTest < UnitTest +class IntegrationTest < ActionDispatch::IntegrationTest include Capybara::DSL + include TestSetupTeardown def register(login: "user@example.com", password: "secret", verify: false) visit "/create-account" From d0422fa75a9ed1797b577af4e2d8b5e4ad310810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janko=20Marohni=C4=87?= Date: Mon, 20 Nov 2023 22:44:11 +0100 Subject: [PATCH 2/5] Drop support for Ruby 2.3 and Ruby 2.4 This way we get access to String#delete_suffix and String#delete_prefix methods. --- .github/workflows/ci.yml | 22 +--------------------- gemfiles/Gemfile.rails-5.0 | 14 ++------------ gemfiles/Gemfile.rails-5.1 | 14 ++------------ gemfiles/Gemfile.rails-5.2 | 14 ++------------ 4 files changed, 7 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a515861..e29fb177 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ["ruby-2.3", "ruby-2.4", "ruby-2.5", "ruby-2.6", "ruby-2.7", "ruby-3.0", "ruby-3.1", "ruby-3.2", "jruby-9.4"] + ruby: ["ruby-2.5", "ruby-2.6", "ruby-2.7", "ruby-3.0", "ruby-3.1", "ruby-3.2", "jruby-9.4"] gemfile: ["rails-5.0", "rails-5.1", "rails-5.2", "rails-6.0", "rails-6.1", "rails-7.0", "rails-7.1", "rails-main"] exclude: - ruby: "ruby-3.2" @@ -62,26 +62,6 @@ jobs: gemfile: "rails-7.1" - ruby: "ruby-2.5" gemfile: "rails-7.0" - - ruby: "ruby-2.4" - gemfile: "rails-main" - - ruby: "ruby-2.4" - gemfile: "rails-7.1" - - ruby: "ruby-2.4" - gemfile: "rails-7.0" - - ruby: "ruby-2.4" - gemfile: "rails-6.1" - - ruby: "ruby-2.4" - gemfile: "rails-6.0" - - ruby: "ruby-2.3" - gemfile: "rails-main" - - ruby: "ruby-2.3" - gemfile: "rails-7.1" - - ruby: "ruby-2.3" - gemfile: "rails-7.0" - - ruby: "ruby-2.3" - gemfile: "rails-6.1" - - ruby: "ruby-2.3" - gemfile: "rails-6.0" env: BUNDLE_GEMFILE: gemfiles/Gemfile.${{ matrix.gemfile }} diff --git a/gemfiles/Gemfile.rails-5.0 b/gemfiles/Gemfile.rails-5.0 index 74388699..e3a8345a 100644 --- a/gemfiles/Gemfile.rails-5.0 +++ b/gemfiles/Gemfile.rails-5.0 @@ -4,20 +4,10 @@ gemspec path: ".." gem "rake", "~> 12.0" gem "minitest", "5.10.3" -gem "warning" if RUBY_VERSION >= "2.4" +gem "warning" gem "rails", "~> 5.0.0" gem "sqlite3", "~> 1.3.6", platforms: :mri gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby -if RUBY_VERSION < "2.5" - gem "loofah", "< 2.20" - gem "nokogiri", "< 1.15" -end - -if RUBY_VERSION.start_with?("2.3.") - gem "sprockets", "~> 3.7.2" - gem "capybara", "~> 3.15.1" -else - gem "capybara" -end +gem "capybara" diff --git a/gemfiles/Gemfile.rails-5.1 b/gemfiles/Gemfile.rails-5.1 index 445165f0..d1c92d84 100644 --- a/gemfiles/Gemfile.rails-5.1 +++ b/gemfiles/Gemfile.rails-5.1 @@ -3,20 +3,10 @@ source "https://rubygems.org" gemspec path: ".." gem "rake", "~> 12.0" -gem "warning" if RUBY_VERSION >= "2.4" +gem "warning" gem "rails", "~> 5.1.0" gem "sqlite3", "~> 1.4", platforms: :mri gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby -if RUBY_VERSION < "2.5" - gem "loofah", "< 2.20" - gem "nokogiri", "< 1.15" -end - -if RUBY_VERSION.start_with?("2.3.") - gem "sprockets", "~> 3.7.2" - gem "capybara", "~> 3.15.1" -else - gem "capybara" -end +gem "capybara" diff --git a/gemfiles/Gemfile.rails-5.2 b/gemfiles/Gemfile.rails-5.2 index 039b7ead..37bf406b 100644 --- a/gemfiles/Gemfile.rails-5.2 +++ b/gemfiles/Gemfile.rails-5.2 @@ -3,20 +3,10 @@ source "https://rubygems.org" gemspec path: ".." gem "rake", "~> 12.0" -gem "warning" if RUBY_VERSION >= "2.4" +gem "warning" gem "rails", "~> 5.2.0" gem "sqlite3", "~> 1.4", platforms: :mri gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby -if RUBY_VERSION < "2.5" - gem "loofah", "< 2.20" - gem "nokogiri", "< 1.15" -end - -if RUBY_VERSION.start_with?("2.3.") - gem "sprockets", "~> 3.7.2" - gem "capybara", "~> 3.15.1" -else - gem "capybara" -end +gem "capybara" From 9c91904ee2daba008197bd02a3f635e3fcd8cf40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janko=20Marohni=C4=87?= Date: Mon, 20 Nov 2023 22:44:48 +0100 Subject: [PATCH 3/5] Remove format from Rodauth routes Format suffixes won't be routed by Rodauth, so we can communicate that when declaring the routes. --- README.md | 26 +++++++++++++------------- lib/rodauth/rails/routing.rb | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3cfa8845..1112825a 100644 --- a/README.md +++ b/README.md @@ -139,19 +139,19 @@ end $ rails routes -g rodauth ``` ``` - Prefix Verb URI Pattern Controller#Action - login GET|POST /login(.:format) rodauth#login - create_account GET|POST /create-account(.:format) rodauth#create_account - verify_account_resend GET|POST /verify-account-resend(.:format) rodauth#verify_account_resend - verify_account GET|POST /verify-account(.:format) rodauth#verify_account - logout GET|POST /logout(.:format) rodauth#logout - remember GET|POST /remember(.:format) rodauth#remember -reset_password_request GET|POST /reset-password-request(.:format) rodauth#reset_password_request - reset_password GET|POST /reset-password(.:format) rodauth#reset_password - change_password GET|POST /change-password(.:format) rodauth#change_password - change_login GET|POST /change-login(.:format) rodauth#change_login - verify_login_change GET|POST /verify-login-change(.:format) rodauth#verify_login_change - close_account GET|POST /close-account(.:format) rodauth#close_account + Prefix Verb URI Pattern Controller#Action + login GET|POST /login rodauth#login + create_account GET|POST /create-account rodauth#create_account + verify_account_resend GET|POST /verify-account-resend rodauth#verify_account_resend + verify_account GET|POST /verify-account rodauth#verify_account + logout GET|POST /logout rodauth#logout + remember GET|POST /remember rodauth#remember +reset_password_request GET|POST /reset-password-request rodauth#reset_password_request + reset_password GET|POST /reset-password rodauth#reset_password + change_password GET|POST /change-password rodauth#change_password + change_login GET|POST /change-login rodauth#change_login + verify_login_change GET|POST /verify-login-change rodauth#verify_login_change + close_account GET|POST /close-account rodauth#close_account ``` We can use these URL helpers to add some basic authentication links to our app: diff --git a/lib/rodauth/rails/routing.rb b/lib/rodauth/rails/routing.rb index 0a501b09..24495228 100644 --- a/lib/rodauth/rails/routing.rb +++ b/lib/rodauth/rails/routing.rb @@ -9,7 +9,7 @@ def rodauth(name = nil, as: name) controller = rodauth.rails_controller.controller_name namespace = rodauth.rails_controller.module_parent_name&.underscore - scope controller: controller, module: namespace, as: as do + scope controller: controller, module: namespace, as: as, format: false do auth_class.route_hash.each do |route_path, route_method| next if route_method.to_s.end_with?("_js") From 117a85a4658c562d8be76d2ce6942a1e6a3b3d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janko=20Marohni=C4=87?= Date: Mon, 20 Nov 2023 22:48:32 +0100 Subject: [PATCH 4/5] Support older Rails versions --- lib/rodauth/rails/feature/base.rb | 4 ++-- lib/rodauth/rails/routing.rb | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/rodauth/rails/feature/base.rb b/lib/rodauth/rails/feature/base.rb index a8bcba59..22dbdec8 100644 --- a/lib/rodauth/rails/feature/base.rb +++ b/lib/rodauth/rails/feature/base.rb @@ -63,9 +63,9 @@ def before_rodauth end def rails_path_parameters - controller = [rails_controller.module_parent_name&.underscore, rails_controller.controller_name].compact.join("/") + controller = rails_controller.name.delete_suffix("Controller").underscore route_method = self.class.route_hash.fetch(request.path.sub(/^#{prefix}/, "")) - action = route_method.to_s.sub(/^handle_/, "") + action = route_method.to_s.delete_prefix("handle_") { controller: controller, action: action } end diff --git a/lib/rodauth/rails/routing.rb b/lib/rodauth/rails/routing.rb index 24495228..9d9966a7 100644 --- a/lib/rodauth/rails/routing.rb +++ b/lib/rodauth/rails/routing.rb @@ -5,16 +5,14 @@ def rodauth(name = nil, as: name) auth_class = Rodauth::Rails.app.rodauth!(name) scope = auth_class.roda_class.new({}) rodauth = auth_class.new(scope) + controller = rodauth.rails_controller.name.delete_suffix("Controller").underscore - controller = rodauth.rails_controller.controller_name - namespace = rodauth.rails_controller.module_parent_name&.underscore - - scope controller: controller, module: namespace, as: as, format: false do + scope controller: controller, as: as, format: false do auth_class.route_hash.each do |route_path, route_method| next if route_method.to_s.end_with?("_js") path = "#{rodauth.prefix}/#{route_path}" - action = route_method.to_s.sub(/\Ahandle_/, "") + action = route_method.to_s.delete_prefix("handle_") verbs = rodauth_verbs(rodauth, route_method) match path, action: action, as: action, via: verbs From 3a77202816766a76644b505701c107a2be286cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janko=20Marohni=C4=87?= Date: Sun, 26 Nov 2023 09:50:48 +0100 Subject: [PATCH 5/5] Enable per-action controller callbacks --- lib/rodauth/rails/feature/callbacks.rb | 2 ++ test/integration/callbacks_test.rb | 22 +++++++++++++------ .../app/controllers/rodauth_controller.rb | 5 +++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/rodauth/rails/feature/callbacks.rb b/lib/rodauth/rails/feature/callbacks.rb index fb0f2cd6..25c714d6 100644 --- a/lib/rodauth/rails/feature/callbacks.rb +++ b/lib/rodauth/rails/feature/callbacks.rb @@ -12,6 +12,8 @@ def _around_rodauth # Runs controller callbacks and rescue handlers around Rodauth actions. def rails_controller_around + rails_controller_instance.instance_variable_set(:@_action_name, rails_path_parameters[:action]) + result = nil rails_controller_rescue do diff --git a/test/integration/callbacks_test.rb b/test/integration/callbacks_test.rb index a84ffbb5..6d083cf3 100644 --- a/test/integration/callbacks_test.rb +++ b/test/integration/callbacks_test.rb @@ -5,18 +5,26 @@ class CallbacksTest < IntegrationTest visit "/login" assert_match "login-form", page.html - assert_equal 200, page.status_code - assert_equal "true", page.response_headers["X-Before-Action"] - assert_equal "true", page.response_headers["X-After-Action"] - assert_equal "true", page.response_headers["X-Before-Around-Action"] - assert_equal "true", page.response_headers["X-After-Around-Action"] + assert_equal 200, page.status_code + assert_equal "true", page.response_headers["X-Before-Action"] + assert_equal "true", page.response_headers["X-After-Action"] + assert_equal "true", page.response_headers["X-Before-Around-Action"] + assert_equal "true", page.response_headers["X-After-Around-Action"] + end + + test "runs callbacks for specific actions" do + visit "/create-account" + assert_equal "true", page.response_headers["X-Before-Specific-Action"] + + visit "/login" + assert_nil page.response_headers["X-Before-Specific-Action"] end test "handles rendering in callback chain" do visit "/login?early_return=true&fail=true" assert_equal "early return", page.html - assert_equal 201, page.status_code - assert_equal "true", page.response_headers["X-Before-Action"] + assert_equal 201, page.status_code + assert_equal "true", page.response_headers["X-Before-Action"] end end diff --git a/test/rails_app/app/controllers/rodauth_controller.rb b/test/rails_app/app/controllers/rodauth_controller.rb index 86546a2f..0f72f20f 100644 --- a/test/rails_app/app/controllers/rodauth_controller.rb +++ b/test/rails_app/app/controllers/rodauth_controller.rb @@ -2,6 +2,7 @@ class RodauthController < ApplicationController before_action :before_route after_action :after_route around_action :around_route + before_action :before_specific_route, only: [:create_account] rescue_from NotImplementedError do render plain: "rescued response", status: 500 @@ -27,6 +28,10 @@ def around_route response.headers["X-After-Around-Action"] = "true" end + def before_specific_route + response.header["X-Before-Specific-Action"] = "true" + end + def some_method "controller method" end