diff --git a/CHANGELOG.md b/CHANGELOG.md index 6968e7c0..7b104052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,16 @@ ### Unreleased * enhancements -* bug fixes + * The Simple Form templates follow the same change from 3.3.0 by using `Log in` and adding + a hint about the minimum password length when `validatable` is enabled (by @aried3r) + * Remove reloading of routes when eager loading is enabled. This change was added during Rails 3 and it doesn't seem to be relevant to currently supported Rails versions (by @fgro) + * Controller generator added as `devise:controllers SCOPE`. You can use the `-c` flag + to pick which controllers (`unlocks`, `confirmations`, etc) you want to generate. (by @Chun-Yang) +* bug fix + * Fixed a regression where the devise generator would fail with a `ConnectionNotEstablished` + exception when executed inside a mountable engine + * Ensure to return symbols in find_scope! fixing a previous regression from 3.3.0 (by @micat) + * Ensure all causes of failed login have the same error message (by @pjungwir) ### 3.3.0 diff --git a/README.md b/README.md index b25ea5ad..c7bdd6d6 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ When you customize your own views, you may end up adding new attributes to forms There are just three actions in Devise that allows any set of parameters to be passed down to the model, therefore requiring sanitization. Their names and the permitted parameters by default are: -* `sign_in` (`Devise::SessionsController#new`) - Permits only the authentication keys (like `email`) +* `sign_in` (`Devise::SessionsController#create`) - Permits only the authentication keys (like `email`) * `sign_up` (`Devise::RegistrationsController#create`) - Permits authentication keys plus `password` and `password_confirmation` * `account_update` (`Devise::RegistrationsController#update`) - Permits authentication keys plus `password`, `password_confirmation` and `current_password` @@ -282,15 +282,25 @@ rails generate devise:views -v registrations confirmations If the customization at the views level is not enough, you can customize each controller by following these steps: -1. Create your custom controller, for example a `Admins::SessionsController`: +1. Create your custom controllers using the generator which requires a scope: + + ```console + rails generate devise:controllers [scope] + ``` + + If you specify `admins` as the scope, controllers will be created in `app/controllers/admins/`. + And the sessions controller will look like this: ```ruby class Admins::SessionsController < Devise::SessionsController + # GET /resource/sign_in + # def new + # super + # end + ... end ``` - Note that in the above example, the controller needs to be created in the `app/controllers/admins/` directory. - 2. Tell the router to use this controller: ```ruby diff --git a/config/locales/en.yml b/config/locales/en.yml index e419f779..fa69b16d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -12,7 +12,7 @@ en: invalid: "Invalid email or password." locked: "Your account is locked." last_attempt: "You have one more attempt before your account is locked." - not_found_in_database: "Invalid email address or password." + not_found_in_database: "Invalid email or password." timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." unconfirmed: "You have to confirm your email address before continuing." diff --git a/lib/devise/mapping.rb b/lib/devise/mapping.rb index de787dc5..5c40f762 100644 --- a/lib/devise/mapping.rb +++ b/lib/devise/mapping.rb @@ -33,7 +33,7 @@ module Devise def self.find_scope!(obj) case obj when String, Symbol - return obj + return obj.to_sym when Class Devise.mappings.each_value { |m| return m.name if obj <= m.to } else diff --git a/lib/devise/rails.rb b/lib/devise/rails.rb index 14de2375..84169810 100644 --- a/lib/devise/rails.rb +++ b/lib/devise/rails.rb @@ -10,9 +10,6 @@ module Devise Devise.warden_config = config end - # Force routes to be loaded if we are doing any eager load. - config.before_eager_load { |app| app.reload_routes! } - initializer "devise.url_helpers" do Devise.include_helpers(Devise::Controllers) end diff --git a/lib/devise/strategies/authenticatable.rb b/lib/devise/strategies/authenticatable.rb index cd3a15ea..1e2055ec 100644 --- a/lib/devise/strategies/authenticatable.rb +++ b/lib/devise/strategies/authenticatable.rb @@ -36,7 +36,6 @@ module Devise result = resource && resource.valid_for_authentication?(&block) if result - decorate(resource) true else if resource @@ -47,7 +46,7 @@ module Devise end # Get values from params and set in the resource. - def decorate(resource) + def remember_me(resource) resource.remember_me = remember_me? if resource.respond_to?(:remember_me=) end diff --git a/lib/devise/strategies/database_authenticatable.rb b/lib/devise/strategies/database_authenticatable.rb index c3828279..2a8e91d0 100644 --- a/lib/devise/strategies/database_authenticatable.rb +++ b/lib/devise/strategies/database_authenticatable.rb @@ -9,6 +9,7 @@ module Devise encrypted = false if validate(resource){ encrypted = true; resource.valid_password?(password) } + remember_me(resource) resource.after_database_authentication success!(resource) end diff --git a/lib/devise/strategies/rememberable.rb b/lib/devise/strategies/rememberable.rb index 1aadde2c..69066273 100644 --- a/lib/devise/strategies/rememberable.rb +++ b/lib/devise/strategies/rememberable.rb @@ -25,15 +25,18 @@ module Devise end if validate(resource) + remember_me(resource) + extend_remember_me_period(resource) success!(resource) end end private - def decorate(resource) - super - resource.extend_remember_period = mapping.to.extend_remember_period if resource.respond_to?(:extend_remember_period=) + def extend_remember_me_period(resource) + if resource.respond_to?(:extend_remember_period=) + resource.extend_remember_period = mapping.to.extend_remember_period + end end def remember_me? diff --git a/lib/generators/active_record/devise_generator.rb b/lib/generators/active_record/devise_generator.rb index 2ab56b3f..7394bd84 100644 --- a/lib/generators/active_record/devise_generator.rb +++ b/lib/generators/active_record/devise_generator.rb @@ -83,7 +83,8 @@ RUBY end def postgresql? - ActiveRecord::Base.connection.adapter_name.downcase == "postgresql" + config = ActiveRecord::Base.configurations[Rails.env] + config && config['adapter'] == 'postgresql' end end end diff --git a/lib/generators/devise/controllers_generator.rb b/lib/generators/devise/controllers_generator.rb new file mode 100644 index 00000000..921c21c7 --- /dev/null +++ b/lib/generators/devise/controllers_generator.rb @@ -0,0 +1,44 @@ +require 'rails/generators/base' + +module Devise + module Generators + class ControllersGenerator < Rails::Generators::Base + CONTROLLERS = %w(confirmations passwords registrations sessions unlocks omniauth_callbacks).freeze + + desc <<-DESC.strip_heredoc + Create inherited Devise controllers in your app/controllers folder. + + User -c to specify which controller you want to overwrite. + If you do no specify a controller, all controllers will be created. + For example: + + rails generate devise:controllers users -c=sessions + + This will create a controller class at app/controllers/users/sessions_controller.rb like this: + + class Users::ConfirmationsController < Devise::ConfirmationsController + content... + end + DESC + + source_root File.expand_path("../../templates/controllers", __FILE__) + argument :scope, required: true, + desc: "The scope to create controllers in, e.g. users, admins" + class_option :controllers, aliases: "-c", type: :array, + desc: "Select specific controllers to generate (#{CONTROLLERS.join(', ')})" + + def create_controllers + @scope_prefix = scope.blank? ? '' : (scope.camelize + '::') + controllers = options[:controllers] || CONTROLLERS + controllers.each do |name| + template "#{name}_controller.rb", + "app/controllers/#{scope}/#{name}_controller.rb" + end + end + + def show_readme + readme "README" if behavior == :invoke + end + end + end +end diff --git a/lib/generators/templates/controllers/README b/lib/generators/templates/controllers/README new file mode 100644 index 00000000..d8fa757f --- /dev/null +++ b/lib/generators/templates/controllers/README @@ -0,0 +1,14 @@ +=============================================================================== + +Some setup you must do manually if you haven't yet: + + Ensure you have overridden routes for generated controllers in your route.rb. + For example: + + Rails.application.routes.draw do + devise_for :users, controllers: { + sessions: 'sessions' + } + end + +=============================================================================== diff --git a/lib/generators/templates/controllers/confirmations_controller.rb b/lib/generators/templates/controllers/confirmations_controller.rb new file mode 100644 index 00000000..480ee9a0 --- /dev/null +++ b/lib/generators/templates/controllers/confirmations_controller.rb @@ -0,0 +1,28 @@ +class <%= @scope_prefix %>ConfirmationsController < Devise::ConfirmationsController + # GET /resource/confirmation/new + # def new + # super + # end + + # POST /resource/confirmation + # def create + # super + # end + + # GET /resource/confirmation?confirmation_token=abcdef + # def show + # super + # end + + # protected + + # The path used after resending confirmation instructions. + # def after_resending_confirmation_instructions_path_for(resource_name) + # super(resource_name) + # end + + # The path used after confirmation. + # def after_confirmation_path_for(resource_name, resource) + # super(resource_name, resource) + # end +end diff --git a/lib/generators/templates/controllers/omniauth_callbacks_controller.rb b/lib/generators/templates/controllers/omniauth_callbacks_controller.rb new file mode 100644 index 00000000..6516213c --- /dev/null +++ b/lib/generators/templates/controllers/omniauth_callbacks_controller.rb @@ -0,0 +1,28 @@ +class <%= @scope_prefix %>OmniauthCallbacksController < Devise::OmniauthCallbacksController + # You should configure your model like this: + # devise :omniauthable, omniauth_providers: [:twitter] + + # You should also create an action method in this controller like this: + # def twitter + # end + + # More info at: + # https://github.com/plataformatec/devise#omniauth + + # GET|POST /resource/auth/twitter + # def passthru + # super + # end + + # GET|POST /users/auth/twitter/callback + # def failure + # super + # end + + # protected + + # The path used when omniauth fails + # def after_omniauth_failure_path_for(scope) + # super(scope) + # end +end diff --git a/lib/generators/templates/controllers/passwords_controller.rb b/lib/generators/templates/controllers/passwords_controller.rb new file mode 100644 index 00000000..3409d435 --- /dev/null +++ b/lib/generators/templates/controllers/passwords_controller.rb @@ -0,0 +1,32 @@ +class <%= @scope_prefix %>PasswordsController < Devise::PasswordsController + # GET /resource/password/new + # def new + # super + # end + + # POST /resource/password + # def create + # super + # end + + # GET /resource/password/edit?reset_password_token=abcdef + # def edit + # super + # end + + # PUT /resource/password + # def update + # super + # end + + # protected + + # def after_resetting_password_path_for(resource) + # super(resource) + # end + + # The path used after sending reset password instructions + # def after_sending_reset_password_instructions_path_for(resource_name) + # super(resource_name) + # end +end diff --git a/lib/generators/templates/controllers/registrations_controller.rb b/lib/generators/templates/controllers/registrations_controller.rb new file mode 100644 index 00000000..3566e453 --- /dev/null +++ b/lib/generators/templates/controllers/registrations_controller.rb @@ -0,0 +1,60 @@ +class <%= @scope_prefix %>RegistrationsController < Devise::RegistrationsController +# before_filter :configure_sign_up_params, only: [:create] +# before_filter :configure_account_update_params, only: [:update] + + # GET /resource/sign_up + # def new + # super + # end + + # POST /resource + # def create + # super + # end + + # GET /resource/edit + # def edit + # super + # end + + # PUT /resource + # def update + # super + # end + + # DELETE /resource + # def destroy + # super + # end + + # GET /resource/cancel + # Forces the session data which is usually expired after sign + # in to be expired now. This is useful if the user wants to + # cancel oauth signing in/up in the middle of the process, + # removing all OAuth session data. + # def cancel + # super + # end + + # protected + + # You can put the params you want to permit in the empty array. + # def configure_sign_up_params + # devise_parameter_sanitizer.for(:sign_up) << :attribute + # end + + # You can put the params you want to permit in the empty array. + # def configure_account_update_params + # devise_parameter_sanitizer.for(:account_update) << :attribute + # end + + # The path used after sign up. + # def after_sign_up_path_for(resource) + # super(resource) + # end + + # The path used after sign up for inactive accounts. + # def after_inactive_sign_up_path_for(resource) + # super(resource) + # end +end diff --git a/lib/generators/templates/controllers/sessions_controller.rb b/lib/generators/templates/controllers/sessions_controller.rb new file mode 100644 index 00000000..7a243f4e --- /dev/null +++ b/lib/generators/templates/controllers/sessions_controller.rb @@ -0,0 +1,25 @@ +class <%= @scope_prefix %>SessionsController < Devise::SessionsController +# before_filter :configure_sign_in_params, only: [:create] + + # GET /resource/sign_in + # def new + # super + # end + + # POST /resource/sign_in + # def create + # super + # end + + # DELETE /resource/sign_out + # def destroy + # super + # end + + # protected + + # You can put the params you want to permit in the empty array. + # def configure_sign_in_params + # devise_parameter_sanitizer.for(:sign_in) << :attribute + # end +end diff --git a/lib/generators/templates/controllers/unlocks_controller.rb b/lib/generators/templates/controllers/unlocks_controller.rb new file mode 100644 index 00000000..0e2cced5 --- /dev/null +++ b/lib/generators/templates/controllers/unlocks_controller.rb @@ -0,0 +1,28 @@ +class <%= @scope_prefix %>UnlocksController < Devise::UnlocksController + # GET /resource/unlock/new + # def new + # super + # end + + # POST /resource/unlock + # def create + # super + # end + + # GET /resource/unlock?unlock_token=abcdef + # def show + # super + # end + + # protected + + # The path used after sending unlock password instructions + # def after_sending_unlock_instructions_path_for(resource) + # super(resource) + # end + + # The path used after unlocking the resource + # def after_unlock_path_for(resource) + # super(resource) + # end +end diff --git a/lib/generators/templates/devise.rb b/lib/generators/templates/devise.rb index a2090e62..589063d0 100644 --- a/lib/generators/templates/devise.rb +++ b/lib/generators/templates/devise.rb @@ -65,7 +65,7 @@ Devise.setup do |config| # :database = Support basic authentication with authentication key + password # config.http_authenticatable = false - # If http headers should be returned for AJAX requests. True by default. + # If 401 status code should be returned for AJAX requests. True by default. # config.http_authenticatable_on_xhr = true # The realm used in Http Basic Authentication. 'Application' by default. diff --git a/lib/generators/templates/simple_form_for/registrations/new.html.erb b/lib/generators/templates/simple_form_for/registrations/new.html.erb index 090fb295..4ea8fb95 100644 --- a/lib/generators/templates/simple_form_for/registrations/new.html.erb +++ b/lib/generators/templates/simple_form_for/registrations/new.html.erb @@ -5,7 +5,7 @@
<%= f.input :email, required: true, autofocus: true %> - <%= f.input :password, required: true %> + <%= f.input :password, required: true, hint: ("#{@minimum_password_length} characters minimum" if @validatable) %> <%= f.input :password_confirmation, required: true %>
diff --git a/lib/generators/templates/simple_form_for/sessions/new.html.erb b/lib/generators/templates/simple_form_for/sessions/new.html.erb index c790b498..31f8a4d4 100644 --- a/lib/generators/templates/simple_form_for/sessions/new.html.erb +++ b/lib/generators/templates/simple_form_for/sessions/new.html.erb @@ -1,4 +1,4 @@ -

Sign in

+

Log in

<%= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
@@ -8,7 +8,7 @@
- <%= f.button :submit, "Sign in" %> + <%= f.button :submit, "Log in" %>
<% end %> diff --git a/test/controllers/url_helpers_test.rb b/test/controllers/url_helpers_test.rb index 23ba5f15..06895b5d 100644 --- a/test/controllers/url_helpers_test.rb +++ b/test/controllers/url_helpers_test.rb @@ -13,6 +13,12 @@ class RoutesTest < ActionController::TestCase assert_equal @controller.send(:"#{prepend_path}#{name}_url", :user), send(:"#{prepend_path}user_#{name}_url") + # With string + assert_equal @controller.send(:"#{prepend_path}#{name}_path", "user"), + send(:"#{prepend_path}user_#{name}_path") + assert_equal @controller.send(:"#{prepend_path}#{name}_url", "user"), + send(:"#{prepend_path}user_#{name}_url") + # Default url params assert_equal @controller.send(:"#{prepend_path}#{name}_path", :user, param: 123), send(:"#{prepend_path}user_#{name}_path", param: 123) diff --git a/test/generators/controllers_generator_test.rb b/test/generators/controllers_generator_test.rb new file mode 100644 index 00000000..11d1c35d --- /dev/null +++ b/test/generators/controllers_generator_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class ControllersGeneratorTest < Rails::Generators::TestCase + tests Devise::Generators::ControllersGenerator + destination File.expand_path("../../tmp", __FILE__) + setup :prepare_destination + + test "Assert no controllers are created with no params" do + run_generator + assert_no_file "app/controllers/sessions_controller.rb" + assert_no_file "app/controllers/registrations_controller.rb" + assert_no_file "app/controllers/confirmations_controller.rb" + assert_no_file "app/controllers/passwords_controller.rb" + assert_no_file "app/controllers/unlocks_controller.rb" + assert_no_file "app/controllers/omniauth_callbacks_controller.rb" + end + + test "Assert all controllers are properly created with scope param" do + run_generator %w(users) + assert_class_names 'users' + + run_generator %w(admins) + assert_class_names 'admins' + end + + test "Assert specified controllers with scope" do + run_generator %w(users -c sessions) + assert_file "app/controllers/users/sessions_controller.rb" + assert_no_file "app/controllers/users/registrations_controller.rb" + assert_no_file "app/controllers/users/confirmations_controller.rb" + assert_no_file "app/controllers/users/passwords_controller.rb" + assert_no_file "app/controllers/users/unlocks_controller.rb" + assert_no_file "app/controllers/users/omniauth_callbacks_controller.rb" + end + + private + + def assert_class_names(scope, options = {}) + base_dir = "app/controllers#{scope.blank? ? '' : ('/' + scope)}" + scope_prefix = scope.blank? ? '' : (scope.camelize + '::') + controllers = options[:controllers] || + %w(confirmations passwords registrations sessions unlocks omniauth_callbacks) + + controllers.each do |c| + assert_file "#{base_dir}/#{c}_controller.rb", /#{scope_prefix + c.camelize}/ + end + end +end diff --git a/test/integration/http_authenticatable_test.rb b/test/integration/http_authenticatable_test.rb index b7e770e1..aad51f62 100644 --- a/test/integration/http_authenticatable_test.rb +++ b/test/integration/http_authenticatable_test.rb @@ -42,7 +42,7 @@ class HttpAuthenticationTest < ActionDispatch::IntegrationTest sign_in_as_new_user_with_http("unknown") assert_equal 401, status assert_equal "application/xml; charset=utf-8", headers["Content-Type"] - assert_match "Invalid email address or password.", response.body + assert_match "Invalid email or password.", response.body end test 'returns a custom response with www-authenticate and chosen realm' do diff --git a/test/mapping_test.rb b/test/mapping_test.rb index 2ec97e19..d22bf0bc 100644 --- a/test/mapping_test.rb +++ b/test/mapping_test.rb @@ -62,6 +62,7 @@ class MappingTest < ActiveSupport::TestCase test 'find scope for a given object' do assert_equal :user, Devise::Mapping.find_scope!(User) assert_equal :user, Devise::Mapping.find_scope!(:user) + assert_equal :user, Devise::Mapping.find_scope!("user") assert_equal :user, Devise::Mapping.find_scope!(User.new) end diff --git a/test/rails_app/db/migrate/20100401102949_create_tables.rb b/test/rails_app/db/migrate/20100401102949_create_tables.rb index 5aaf7364..fd90b504 100644 --- a/test/rails_app/db/migrate/20100401102949_create_tables.rb +++ b/test/rails_app/db/migrate/20100401102949_create_tables.rb @@ -33,7 +33,7 @@ class CreateTables < ActiveRecord::Migration t.string :unlock_token # Only if unlock strategy is :email or :both t.datetime :locked_at - t.timestamps + t.timestamps null: false end create_table :admins do |t| @@ -60,7 +60,7 @@ class CreateTables < ActiveRecord::Migration ## Attribute for testing route blocks t.boolean :active, default: false - t.timestamps + t.timestamps null: false end end diff --git a/test/test_helper.rb b/test/test_helper.rb index dd200611..49553d82 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -27,3 +27,4 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } require "rails/generators/test_case" require "generators/devise/install_generator" require "generators/devise/views_generator" +require "generators/devise/controllers_generator"