Compare commits

...

5 Commits

Author SHA1 Message Date
Carlos Antonio da Silva
d5a48b49dc Release v4.9.4 2024-04-10 09:27:12 -03:00
Carlos Antonio da Silva
bab47e1c1f Adds Ruby 3.3 to CI on 4-stable
Related: #5668
2024-04-09 17:12:52 -03:00
Carlos Antonio da Silva
cee7457d7f Bump year [ci skip] 2024-04-09 16:58:42 -03:00
Carlos Antonio da Silva
95ed7d3dd8 Merge pull request #5641 from henryaj/patch-1
Fix README anchor link [ci skip]
2023-10-13 11:57:29 -03:00
Carlos Antonio da Silva
edffc79bf0 Respect locale set by controller in the failure app (#5567)
A common usage of I18n with different locales is to create some around
callback in the application controller that sets the locale for the
entire action, via params/url/user/etc., which ensure the locale is
respected for the duration of that action, and resets at the end.

Devise was not respecting the locale when the authenticate failed and
triggered the failure app, because that happens in a warden middleware
right up in the change, by that time the controller around callback had
already reset the locale back to its default, and the failure app would
just translate flash messages using the default locale.

Now we are passing the current locale down to the failure app via warden
options, and wrapping it with an around callback, which makes the
failure app respect the set I18n locale by the controller at the time
the authentication failure is triggered, working as expected. (much more
like a normal controller would.)

I chose to introduce a callback in the failure app so we could wrap the
whole `respond` action processing rather than adding individual `locale`
options to the `I18n.t` calls, because that should ensure other possible
`I18n.t` calls from overridden failure apps would respect the set locale
as well, and makes it more like one would implement in a controller. I
don't recommend people using callbacks in their own failure apps though,
as this is not going to be documented as a "feature" of failures apps,
it's considered "internal" and could be refactored at any point.

It is possible to override the locale with the new `i18n_locale` method,
which simply defaults to the passed locale from the controller.

Closes #5247
Closes #5246

Related to: #3052, #4823, and possible others already closed.
Related to warden: (may be closed there afterwards)
https://github.com/wardencommunity/warden/issues/180
https://github.com/wardencommunity/warden/issues/170
2023-10-13 11:19:45 -03:00
15 changed files with 96 additions and 13 deletions

View File

@@ -17,6 +17,7 @@ jobs:
- gemfiles/Gemfile-rails-4-2
- gemfiles/Gemfile-rails-4-1
ruby:
- '3.3'
- '3.2'
- '3.1'
- '3.0'
@@ -31,6 +32,10 @@ jobs:
- DEVISE_ORM=active_record
- DEVISE_ORM=mongoid
exclude:
- gemfile: gemfiles/Gemfile-rails-main
ruby: '2.7' # Rails > 7.1 supports Ruby >= 3.1
- gemfile: gemfiles/Gemfile-rails-main
ruby: '3.0' # Rails > 7.1 supports Ruby >= 3.1
- gemfile: Gemfile
ruby: '2.6'
- gemfile: Gemfile
@@ -83,6 +88,8 @@ jobs:
ruby: '2.1'
- gemfile: gemfiles/Gemfile-rails-6-1
env: DEVISE_ORM=mongoid
- gemfile: gemfiles/Gemfile-rails-6-0
ruby: '3.3'
- gemfile: gemfiles/Gemfile-rails-6-0
ruby: '3.2'
- gemfile: gemfiles/Gemfile-rails-6-0
@@ -97,6 +104,8 @@ jobs:
ruby: '2.1'
- gemfile: gemfiles/Gemfile-rails-6-0
env: DEVISE_ORM=mongoid
- gemfile: gemfiles/Gemfile-rails-5-2
ruby: '3.3'
- gemfile: gemfiles/Gemfile-rails-5-2
ruby: '3.2'
- gemfile: gemfiles/Gemfile-rails-5-2
@@ -111,6 +120,8 @@ jobs:
ruby: '2.1'
- gemfile: gemfiles/Gemfile-rails-5-2
env: DEVISE_ORM=mongoid
- gemfile: gemfiles/Gemfile-rails-5-1
ruby: '3.3'
- gemfile: gemfiles/Gemfile-rails-5-1
ruby: '3.2'
- gemfile: gemfiles/Gemfile-rails-5-1
@@ -123,6 +134,8 @@ jobs:
ruby: '2.1'
- gemfile: gemfiles/Gemfile-rails-5-1
env: DEVISE_ORM=mongoid
- gemfile: gemfiles/Gemfile-rails-5-0
ruby: '3.3'
- gemfile: gemfiles/Gemfile-rails-5-0
ruby: '3.2'
- gemfile: gemfiles/Gemfile-rails-5-0
@@ -135,6 +148,8 @@ jobs:
ruby: '2.1'
- gemfile: gemfiles/Gemfile-rails-5-0
env: DEVISE_ORM=mongoid
- gemfile: gemfiles/Gemfile-rails-4-2
ruby: '3.3'
- gemfile: gemfiles/Gemfile-rails-4-2
ruby: '3.2'
- gemfile: gemfiles/Gemfile-rails-4-2
@@ -145,6 +160,8 @@ jobs:
ruby: '2.7'
- gemfile: gemfiles/Gemfile-rails-4-2
ruby: '2.6'
- gemfile: gemfiles/Gemfile-rails-4-1
ruby: '3.3'
- gemfile: gemfiles/Gemfile-rails-4-1
ruby: '3.2'
- gemfile: gemfiles/Gemfile-rails-4-1

View File

@@ -1,3 +1,11 @@
### 4.9.4 - 2024-04-10
* enhancements
* Add support for Ruby 3.3. (no changes needed)
* bug fixes
* Respect locale set by controller in failure app. Devise will carry over the current I18n.locale option when triggering authentication, and will wrap the failure app call with it. [#5567](https://github.com/heartcombo/devise/pull/5567)
### 4.9.3 - 2023-10-11
* enhancements

View File

@@ -10,7 +10,7 @@ GIT
PATH
remote: .
specs:
devise (4.9.3)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)

View File

@@ -1,4 +1,4 @@
Copyright 2020-2023 Rafael França, Leonardo Tegon, Carlos Antônio da Silva.
Copyright 2020-2024 Rafael França, Leonardo Tegon, Carlos Antônio da Silva.
Copyright 2009-2019 Plataformatec.
Permission is hereby granted, free of charge, to any person obtaining

View File

@@ -46,7 +46,7 @@ It's composed of 10 modules:
- [Integration tests](#integration-tests)
- [OmniAuth](#omniauth)
- [Configuring multiple models](#configuring-multiple-models)
- [ActiveJob Integration](#activejob-integration)
- [Active Job Integration](#active-job-integration)
- [Password reset tokens and Rails logs](#password-reset-tokens-and-rails-logs)
- [Other ORMs](#other-orms)
- [Rails API mode](#rails-api-mode)
@@ -767,6 +767,6 @@ https://github.com/heartcombo/devise/graphs/contributors
## License
MIT License. Copyright 2020-2023 Rafael França, Leonardo Tegon, Carlos Antônio da Silva. Copyright 2009-2019 Plataformatec.
MIT License. Copyright 2020-2024 Rafael França, Leonardo Tegon, Carlos Antônio da Silva. Copyright 2009-2019 Plataformatec.
The Devise logo is licensed under [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](https://creativecommons.org/licenses/by-nc-nd/4.0/).

View File

@@ -45,7 +45,7 @@ class Devise::SessionsController < DeviseController
end
def auth_options
{ scope: resource_name, recall: "#{controller_path}#new" }
{ scope: resource_name, recall: "#{controller_path}#new", locale: I18n.locale }
end
def translation_scope

View File

@@ -46,6 +46,7 @@ module Devise
mappings.unshift mappings.delete(favorite.to_sym) if favorite
mappings.each do |mapping|
opts[:scope] = mapping
opts[:locale] = I18n.locale
warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end
end
@@ -115,6 +116,7 @@ module Devise
class_eval <<-METHODS, __FILE__, __LINE__ + 1
def authenticate_#{mapping}!(opts = {})
opts[:scope] = :#{mapping}
opts[:locale] = I18n.locale
warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end

View File

@@ -18,6 +18,11 @@ module Devise
delegate :flash, to: :request
include AbstractController::Callbacks
around_action do |failure_app, action|
I18n.with_locale(failure_app.i18n_locale, &action)
end
def self.call(env)
@respond ||= action(:respond)
@respond.call(env)
@@ -107,7 +112,7 @@ module Devise
options[:default] = [message]
auth_keys = scope_class.authentication_keys
keys = (auth_keys.respond_to?(:keys) ? auth_keys.keys : auth_keys).map { |key| scope_class.human_attribute_name(key) }
options[:authentication_keys] = keys.join(I18n.translate(:"support.array.words_connector"))
options[:authentication_keys] = keys.join(I18n.t(:"support.array.words_connector"))
options = i18n_options(options)
I18n.t(:"#{scope}.#{message}", **options)
@@ -116,6 +121,10 @@ module Devise
end
end
def i18n_locale
warden_options[:locale]
end
def redirect_url
if warden_message == :timeout
flash[:timedout] = true if is_flashing_format?

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Devise
VERSION = "4.9.3".freeze
VERSION = "4.9.4".freeze
end

View File

@@ -64,30 +64,30 @@ class ControllerAuthenticatableTest < Devise::ControllerTestCase
end
test 'proxy authenticate_user! to authenticate with user scope' do
@mock_warden.expects(:authenticate!).with({ scope: :user })
@mock_warden.expects(:authenticate!).with({ scope: :user, locale: :en })
@controller.authenticate_user!
end
test 'proxy authenticate_user! options to authenticate with user scope' do
@mock_warden.expects(:authenticate!).with({ scope: :user, recall: "foo" })
@mock_warden.expects(:authenticate!).with({ scope: :user, recall: "foo", locale: :en })
@controller.authenticate_user!(recall: "foo")
end
test 'proxy authenticate_admin! to authenticate with admin scope' do
@mock_warden.expects(:authenticate!).with({ scope: :admin })
@mock_warden.expects(:authenticate!).with({ scope: :admin, locale: :en })
@controller.authenticate_admin!
end
test 'proxy authenticate_[group]! to authenticate!? with each scope' do
[:user, :admin].each do |scope|
@mock_warden.expects(:authenticate!).with({ scope: scope })
@mock_warden.expects(:authenticate!).with({ scope: scope, locale: :en })
@mock_warden.expects(:authenticate?).with(scope: scope).returns(false)
end
@controller.authenticate_commenter!
end
test 'proxy authenticate_publisher_account! to authenticate with namespaced publisher account scope' do
@mock_warden.expects(:authenticate!).with({ scope: :publisher_account })
@mock_warden.expects(:authenticate!).with({ scope: :publisher_account, locale: :en })
@controller.authenticate_publisher_account!
end

View File

@@ -200,6 +200,13 @@ class FailureTest < ActiveSupport::TestCase
assert_equal 'User Steve does not exist', @request.flash[:alert]
end
test 'respects the i18n locale passed via warden options when redirecting' do
call_failure('warden' => OpenStruct.new(message: :invalid), 'warden.options' => { locale: :"pt-BR" })
assert_equal 'Email ou senha inválidos.', @request.flash[:alert]
assert_equal 'http://test.host/users/sign_in', @response.second["Location"]
end
test 'uses the proxy failure message as string' do
call_failure('warden' => OpenStruct.new(message: 'Hello world'))
assert_equal 'Hello world', @request.flash[:alert]
@@ -284,6 +291,12 @@ class FailureTest < ActiveSupport::TestCase
assert_match '<error>Invalid Email or password.</error>', @response.third.body
end
test 'respects the i18n locale passed via warden options when responding to HTTP request' do
call_failure('formats' => Mime[:json], 'warden' => OpenStruct.new(message: :invalid), 'warden.options' => { locale: :"pt-BR" })
assert_equal %({"error":"Email ou senha inválidos."}), @response.third.body
end
context 'on ajax call' do
context 'when http_authenticatable_on_xhr is false' do
test 'dont return 401 with navigational formats' do
@@ -372,6 +385,18 @@ class FailureTest < ActiveSupport::TestCase
end
end
test 'respects the i18n locale passed via warden options when recalling original controller' do
env = {
"warden.options" => { recall: "devise/sessions#new", attempted_path: "/users/sign_in", locale: :"pt-BR" },
"devise.mapping" => Devise.mappings[:user],
"warden" => stub_everything
}
call_failure(env)
assert_includes @response.third.body, '<h2>Log in</h2>'
assert_includes @response.third.body, 'Email ou senha inválidos.'
end
# TODO: remove conditional/else when supporting only responders 3.1+
if ActionController::Responder.respond_to?(:error_status=)
test 'respects the configured responder `error_status` for the status code' do
@@ -431,6 +456,7 @@ class FailureTest < ActiveSupport::TestCase
assert_equal "yes it does", Devise::FailureApp.new.lazy_loading_works?
end
end
context "Without Flash Support" do
test "returns to the default redirect location without a flash message" do
call_failure request_klass: RequestWithoutFlashSupport

View File

@@ -273,6 +273,15 @@ class AuthenticationRedirectTest < Devise::IntegrationTest
assert_contain 'You need to sign in or sign up before continuing.'
end
test 'redirect from warden respects i18n locale set at the controller' do
get admins_path(locale: "pt-BR")
assert_redirected_to new_admin_session_path
follow_redirect!
assert_contain 'Para continuar, faça login ou registre-se.'
end
test 'redirect to default url if no other was configured' do
sign_in_as_user
assert_template 'home/index'

View File

@@ -1,8 +1,15 @@
# frozen_string_literal: true
class AdminsController < ApplicationController
around_action :set_locale
before_action :authenticate_admin!
def index
end
private
def set_locale
I18n.with_locale(params[:locale] || I18n.default_locale) { yield }
end
end

View File

@@ -0,0 +1,5 @@
pt-BR:
devise:
failure:
invalid: "%{authentication_keys} ou senha inválidos."
unauthenticated: "Para continuar, faça login ou registre-se."

View File

@@ -10,7 +10,7 @@ require "rails_app/config/environment"
require "rails/test_help"
require "orm/#{DEVISE_ORM}"
I18n.load_path << File.expand_path("../support/locale/en.yml", __FILE__)
I18n.load_path.concat Dir["#{File.dirname(__FILE__)}/support/locale/*.yml"]
require 'mocha/minitest'
require 'timecop'