Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Rails URL helpers #245

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 1 addition & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 }}

Expand Down
76 changes: 43 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
```

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 %>
```

Expand Down Expand Up @@ -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
Expand All @@ -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: "[email protected]", password: "secret123")
RodauthApp.rodauth(:admin).verify_account(account_login: "[email protected]")

# 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
Expand Down Expand Up @@ -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/
Expand Down
14 changes: 2 additions & 12 deletions gemfiles/Gemfile.rails-5.0
Original file line number Diff line number Diff line change
Expand Up @@ -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"
14 changes: 2 additions & 12 deletions gemfiles/Gemfile.rails-5.1
Original file line number Diff line number Diff line change
Expand Up @@ -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"
14 changes: 2 additions & 12 deletions gemfiles/Gemfile.rails-5.2
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 4 additions & 0 deletions lib/generators/rodauth/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
6 changes: 0 additions & 6 deletions lib/rodauth/rails/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions lib/rodauth/rails/feature/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.name.delete_suffix("Controller").underscore
route_method = self.class.route_hash.fetch(request.path.sub(/^#{prefix}/, ""))
action = route_method.to_s.delete_prefix("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)
Expand Down
2 changes: 2 additions & 0 deletions lib/rodauth/rails/feature/callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/rodauth/rails/feature/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions lib/rodauth/rails/railtie.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "rodauth/rails/middleware"
require "rodauth/rails/controller_methods"
require "rodauth/rails/test"
require "rodauth/rails/routing"

require "rails"

Expand All @@ -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?
Expand All @@ -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
42 changes: 42 additions & 0 deletions lib/rodauth/rails/routing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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.name.delete_suffix("Controller").underscore

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.delete_prefix("handle_")
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
12 changes: 0 additions & 12 deletions lib/rodauth/rails/tasks.rake

This file was deleted.

Loading